Scanned books are commonly distributed as DjVu, which the scanner only stored as a supplement.
Decode pages on the server with DjVuLibre so DjVu imports with a cover and page count, opens in
the image reader with its embedded OCR text, and can be the preferred format of an ordinary email
recipient. Migration 0113 widens the recipient format check to allow it.
Decoding runs on validated private snapshots behind a bounded, reader-first queue, and
revalidation and cached pages are answered without a decoder. Released iOS builds cannot open
DjVu, so series continuation offers it only to callers sending includeDjvu=true, and the
dashboard and notebook read file never name it. The first full scan after upgrading promotes
stored .djvu supplements to content.
Use the source finish day when importing completed KOReader books so
completion timelines reflect reading history instead of the sync date.
Preserve newer manual edits and retain fallback behavior for unusable dates.
Closes#1783
Copy unchanged ZIP records without inflating and recompressing book content
so metadata saves avoid processing every image, chapter, and font.
Use bounded file I/O and validate archive structure while preserving atomic
replacement and original entry compression. Cover ZIP64, descriptor variants,
byte preservation, repeated saves, and filesystem failures in regression tests.
Closes#1798
Resolve OPF and Dublin Core namespaces so valid prefixed metadata retains
book language and avoids incorrect English dictionary lookups.
Preserve element order and identifiers, and cover reader metadata and
narration with unit and HTTP regression tests.
Closes#1789
Library access roles had no effect on metadata editing: only the global Edit metadata permission
was checked, so a Viewer could edit every library they could see and an Editor could not be
limited to one library. Require Editor or Owner on the book's library for every metadata write,
including bulk edits, locks, covers, file renames, entity manager changes and author edits, and
hide the matching actions in the web client.
Keep existing access levels when an admin saves a user's library list. The old replace reset every
row to Viewer, which would now silently revoke editing on each user save.
Existing grants are Viewer, so after upgrading, admins must set Editor on each library where a
non-superuser should keep editing metadata. The released iOS app already gates editing on Editor.
Closes#1778
The authorization matrix fails for any live route that is neither inventoried nor in its frozen
gap baseline, and GET /server-identity arrived without an entry. Record it as public with no
permission or library access, which is exactly what the route serves.
Local connection-address builds created server_identity from a migration whose ledger row is
dated after 0109 to 0111. Drizzle applies only migrations newer than the newest recorded one, so
a database that ran one of those builds would skip the release migrations recorded before it,
including 0111_add_koreader_session_tombstones, on its next upgrade.
Drop that one record by its exact hash before migrating. 0112 creates the same table only when it
is missing, so the installation UUID survives and the table is recorded under its released
migration. A journal that owns the hash keeps the record, and builds without this step never
touch it, so the server that ran the unreleased build still restarts normally until upgraded.
The iOS app lets an account reach one server through several addresses, such as a home network
IP and a public name. Before it sends credentials to an alternative address it needs to know the
address leads to the same installation, so expose a public, credential-free GET /server-identity
returning a persisted UUID, and add the same optional instanceId to app-info with the
server-connection-identity feature flag.
The UUID lives in a singleton server_identity row shared by every replica, created on first use
and cached per process. An unreadable identity omits instanceId instead of failing app-info,
which clients rely on for feature detection. Existing routes and bodies are unchanged and the
web client does not call the new route; the iOS app does.
The migration uses IF NOT EXISTS because an unreleased build created the same table as 0109.
Keep the narration marker a reading progress write leaves out. The
save turned an omitted positionSeconds, mediaOverlayFragment or
mediaOverlaySectionIndex into null and wrote it, so any client saving
only its text position erased the marker another client had left. An
explicit null still clears it. The audiobook-to-ebook sync now moves
narrationUpdatedAt and the narration percentage with the marker it
writes, so a comparison of stamps no longer prefers an older text
position.
Answer TTS requests as their types say. A position never saved is
JSON null rather than an empty 200 body, and the book preferences PUT
answers providerId, voiceId, speed and bookId, with the provider as a
string. TtsPosition gains the updatedAt the route has always sent.
Refuse with a 400 what used to reach the database as a 500: a null
speed for the account (now left unchanged), a provider id that names
no provider, a voiceId over 200 characters, a cfi, fragment or Kobo
location longer than its column, and a fractional or oversized
section index. Voices for a provider id that is not a number answer
404.
Match the reader's blocks in chapter text: tag names now match whole,
so <picture>, <param>, <link>, <track> and SVG shapes no longer open
blocks foliate does not count. The chapter href is percent-decoded,
and cached text is keyed by the file's modified time.
Build a narration playlist once per copy of the file instead of on
every overlay audio request, answer an unsatisfiable range with 416
and ignore an unreadable Range header. The iOS app now plays online
read-along audio from this route in 512 KB ranges, so it is asked
several times per clip.
Give plain synthesis the captioned path's 120 s limit beside the
client's own cancel signal, and voice listing and connection tests a
30 s one.
Fix the server gaps found while porting book detail actions to Android,
checked against the iOS app:
- audit: record a metadata refresh only when it writes; a preview,
which every editor lookup takes, was logged as "Refreshed metadata"
- personal note: leave the note alone when the body has no note key,
as the other partial routes do; null still clears it, and the web
and every iOS build send the key
- covers: answer re-extract-cover with 409 when every slot it would
write is locked, as the other cover writes do, instead of an empty
count a client cannot tell from a file with no art
- types: describe the cover search response as the bare array the
route returns
Fix the server and web gaps found while porting the PDF and comic
readers and offline downloads to Android, checked against the iOS app:
- KOReader: answer a pull for a PDF or comic with the page number when a
BookOrbit client wrote last, instead of a percentage with no page
- comics: key the zip offset, RAR and CB7 caches by file id and
modification time, so pages are not served from stale offsets after a
metadata write-back rewrites the archive
- comics: answer an unreadable archive with 422 rather than a page count
of 0, and extract CB7 with its folders so same-named pages in
different folders no longer overwrite each other
- book files: send an ETag and Last-Modified on serve and download and
honour If-None-Match and If-Range, clamp a range that runs past the
end, answer suffix ranges with 206, and honour Range on download,
which advertised it but sent the whole file
- book files: leave raw file routes uncompressed, so FB2 and unknown
formats keep their Content-Length
- progress: reject a fractional or negative page number with 400 rather
than failing with 500 at the database
- types: declare the comic page-count response shared by the web and
the apps
- web: open a percentage-only PDF position at its estimated page, and
flush pending PDF and comic progress before the reading session ends
Preserve reset-hold notices when recovery requests fail and prevent
stale responses from replacing another book's progress. Refresh holds
after permission changes and rereads, and cover recovery and session
deletion with regression tests.
Ref #1355
Deleting a KOReader session left its page events stored, so the next upload within the
session gap re-derived it. Record the deleted span in koreader_session_tombstones and skip
those events during derivation. Show the reset-hold notice on the Reading Log as well,
since Reset is reachable there and a held device otherwise reads as unexplained progress.
Refs #1355
A stream copy of a large audiobook onto a network share outlived the fixed 60s ffmpeg timeout
and failed the write. FFmpeg now reports progress and is stopped only once its output stops
growing for max(2 min, bytes written at 1 MiB/s), which also covers the silent flush on close.
Fixes#1769
Honor Retry-After for Newznab searches and NZB downloads so transient
rate limits do not immediately end automatic fulfillment. Bound retries
and preserve deadlines without restarting the outer retry budget.
Add unit coverage for rate-limit headers, exhaustion, cancellation,
redirects, and suppression of nested retries. Verify automatic grab
recovery with a real HTTP reproduction.
Fixes#1749
A voice or speed change invalidated the playing block's end handler, so the next block was never
requested, and a change during loading left the player loading forever. Hand the playing block over
to the new settings, refetch any block playback was waiting on, and cancel superseded requests in
the client and at the provider. Audio that arrives during a pause no longer resumes playback.
Fixes#1734
Fold six tabs into Recipients, Mail servers, Templates and History, where only problems get
colour and one star sets each default through email_preferences. Replace the send dialog with
a sheet that preselects the default recipient and follows the send. Server additions are
optional fields and routes, so released iOS builds keep working.
Probe equally ranked comic runs within the existing search budget to prevent reprints from hiding original issues. Require an unambiguous result for automatic matching while preserving manual selection and saved-ID refreshes.
Add regression coverage for the reported Generation X match, padded issue numbers, competing runs, provider failures, deadlines, and cache isolation.
Fixes#1238
Read every EPUB 3 role refinement so authors and narrators survive
regardless of role order, including creators who hold both roles.
Preserve EPUB 2 role precedence and contributor handling, and add
regression coverage for ordering, normalization, and deduplication.
Fixes#1344
A cached Hardcover match outranked a corrected Hardcover ID or ISBN, so progress kept going to the
wrong book. Record the identifiers each link came from and re-check it when they change, honour the
metadata edition on a match, and release the old book's read links so they are never reused.
Closes#1488
FFmpeg picks its ipod muxer from the .m4b temp name, and that muxer rejects the
MP3 an Audiobookshelf copy merge leaves inside an MP4. Probe the audio codec and
use the mp4 muxer for anything but AAC, ALAC and AC-3, so other files keep their
iTunes file type. The rename collision log now names the path that collided.
Closes#1593
Keep disabled Prowlarr limits from being reapplied through tracker metadata,
cached releases, or stale source rows after a failed synchronization.
Cover policy changes, synchronization, both torrent transports, client defaults,
and the PostgreSQL join with regression tests.
Closes#1625
Nest 12 runs global guards on gateway messages, and the HTTP JWT guard
crashed every subscribe on a missing Authorization header. Skip it for
sockets, which authenticate in handleConnection, and make subscribe
handlers wait for that handshake. Library rooms now require library
access, and a real-socket test runs the app's global guards.
Closes#1743
Audiobookshelf reports a progress row's creation time as its start, while
clients set the finish date freely, so a backfilled finish can predate it.
One such row broke user_book_status's date check and the whole status batch
with it. Keep the finish date and drop the start, and store the database's
reason on a failed run instead of the failed SQL.
Closes#1678
The iOS library filter offers languages as chips and needs the whole list, not a typeahead. A blank
term on /metadata/languages now returns every language value on a visible book, most common first
and capped at 200, under the same library and content-filter scope as the search. Every other
suggestion endpoint still answers a blank term with nothing.
The web client never sends a blank term. Released iOS builds do, when the Smart Shelf language
picker opens, and now see the list there instead of a search prompt. The iOS filter calls this
route too, though nothing in the web client shows it.
Ref bookorbit/bookorbit-ios-support#54
epubBuilder writes the OPF root as `mlns=` for `xmlns=`. Once a metadata save
bound `opf:` on <package>, foliate matched OPF elements by namespace, found no
spine, and failed with "reading 'children'". Match by namespace only when the
root is in it, and have the writer add the missing default namespace so
rewritten files also open in released iOS builds.
Closes#1680
The player follows each track's stored sort order, which overlapping
folder scans, Add File, and imports joining a book could leave out of
step with the file names the Files tab shows. Realign a book's tracks
after each of those writes, under the book row lock.
Closes#1681
Book Dock finalize, which also files book requests, left each audio file's
duration empty until the next library scan. The player derives progress only
from those, so a filed audiobook stayed at 0% and never reached Continue
Listening. Finalize now measures them once metadata is applied.
Refs #1757
Keep downloads active after transient tracker failures so later completions
reach the importer. Reserve early failure for explicit rejections and import
completed content even when the tracker still reports an old rejection.
Cover timeout recovery, transient failures, and explicit rejections with unit
tests.
Closes#1607
Map <Genre> to genres and <Tags> to tags instead of using tags as a genre
fallback, which dropped them whenever a Genre was present. Read the
ComicBookInfo genre field so those comics keep their genres.
Fixes#1572
Descriptions are stored as HTML but the feed sent them escaped under
type="text", so clients that honour the type, such as Foliate, showed the
tags literally. Flatten them with htmlToPlainText, keeping line breaks.
Fixes#1569
Bump every direct dependency to its newest same-major release and move the
node base image to 26.11.1, which invalidates the cached apk layer and picks
up tiff 4.7.2 for CVE-2026-4775. Rebuild all six kepubify binaries from the
pinned upstream commit with Go 1.27.2 for CVE-2026-78667, CVE-2026-78669 and
CVE-2026-97031, keeping v4-dev so rendition hashes stay stable.
Write the tags FFmpeg's iTunes mode drops (subtitle, publisher, language,
series, ASIN) as freeform atoms, and carry over the source file's own
freeform tags such as Audible's. Covered M4B and M4A writes lost them.
Ref #1744
Add original and text-only EPUB delivery choices across Kobo, KOReader,
OPDS, web downloads and email while keeping source files unchanged.
Persist server defaults, device overrides and captured artifact identities
so retries and guided replacement retain the selected rendition.
Preserve saved progress across first opens, replacement resets and stale
updates, and project annotations against the copy delivered to each device.
Handle XML resource references, large inodes, retry backoff and background
worker state correctly. Consolidate the schema changes into one migration
and retain released-client request and response compatibility.
Verify with the full server and client checks, delivery and sync HTTP suites,
independent code reviews, and live KOReader download/settings/reader proof.
Fixes#1496Closes#1603Closes#1714Fixes#1723
Ref #1633
Ref #1508
Ref #1621
Ref #1136
Repeated searches and refreshes asked every provider again, which is slow and runs into provider
rate limits. Keep each provider's answer in Postgres, shared by all users, and reuse it until an
admin-set lifetime runs out. Search screens mark saved results and offer a fresh search, and
explicit refreshes stay live, falling back to a saved answer only when a provider fails.
Empty answers are only kept for API providers that report failures reliably, and a cache-only
request never calls a provider in cooldown. Book Dock's manual refetch stays live-only because
the dock has nowhere to report a fallback. Released iOS builds keep working: every new request
field is optional and newer clients gate it on the metadata-search-cache server feature.
Keep books and comic archives under the existing ebook request kind so searches, ownership checks,
format preferences and release profiles agree without changing the API contract. Cover the combined
formats and library access rules with unit and end-to-end tests.
Ref #1748
Share the saved upload limit with validation so changes apply without a page reload.
Seed new installations with a 4096 MB limit while preserving existing values.
Fixes#1733
Each save left the previous save's entries behind when a package bound the OPF namespace to a
prefix other than opf, added another title refinement on EPUB 3, and kept Dublin Core written
without the dc prefix beside its replacement. Match elements regardless of prefix and remove the
refinements of whatever is replaced.
Stop both OPF parsers reading untouched text as numbers and re-escaping character references,
which dropped leading zeros from identifiers the writer was meant to leave alone.
Fixes#1706
Add a Style setting beside mode, accent, radius and background. Nostalgia restyles the app through
tokens only (neutral surface palettes, system type, flat shell, top glow) and pins background,
opacity and brightness. Add a top glow reach slider for both styles. Tidy the theme layer first:
charts read live tokens, bare `rounded` follows the radius scale, one shared sign-in toolbar.
Also carries the settings page header redesign.
Lead fetches with the primary file, give Book Dock its library's rules and read-along media,
default audiobook fields to sources that carry them, and flag providers that never fill a field.
Joining a format now fills gaps only and respects locks; a move picks its primary as a scan would.
Link two ordinary requests as companions, filed into one book, with a Both search,
grouped list rows, a tabbed release picker and a book-page request for the missing
format. Retire comic requests, keeping old rows readable. File Book Dock covers by the
field rules with a shape check, and re-pick a moved book's primary by its new library.
Pick the face slot from the primary file's medium instead of the library's
ratio, and send that frame per book on cards, detail, notebook labels and
recommendations. Grids keep uniform tiles, and the library setting becomes
Grid shape.
Use separate title and author parameters to match the current upstream search form and restore cover results. Add regression coverage for title-only searches, whitespace trimming, and query encoding.
Use file identities for root-level imports and renames to match scanner
grouping and prevent unrelated books from sharing metadata.
Cover repeated scans, metadata write-back, and stable reading and file
identities with regression tests.
Fixes#1599
Return matching item counts with create and update responses so the sidebar
can display the saved scope count without requiring a page reload.
Reuse the listing count logic and verify visibility and shared sidebar state.
Carry ISBN-10 and ISBN-13 independently from accepted providers through
Auto-fill previews, saves and background refreshes. Preserve existing
identifiers and field locks, validate provider checksums and avoid
combining different editions.
Classify embedded ISBNs correctly and support ISBN-10 fallback in
search, Book Dock confidence and file naming.
The walker sorted a book's files by file name, so tracks in disc folders that reuse names, such as
CD 1/01.mp3 and CD 2/01.mp3, interleaved in playback, merged chapters, and written track numbers.
Order them by their whole path instead, everywhere a track order is chosen.
A rescan that only reorders a book's existing tracks now moves every audio bookmark and the
listening progress percentage to the same moment of the same track, and rebuilds merged chapters.
Disc folders are never skipped by incremental scans, so existing libraries are fixed by their next
scan.
Also inventory the notebook and dashboard highlights routes in the authorization matrix, and pin the
app locale in two date tests that depended on the machine's locale.
Books often carry several files, such as an EPUB beside a PDF or an audiobook split into tracks.
Write-back reached only the primary file and the audio tracks, so the other formats kept stale
metadata.
Add an opt-in per-library mode that writes every content file of a book. Target selection,
capability reporting and the Files tab now share one role-aware selector, covers resolve per
medium, and results report per-format counts. Read-along EPUBs get their own toggle, off by
default, and their own size limit of 1000 MB, so they no longer follow the EPUB settings.
Live testing on copies of real libraries surfaced write-path bugs, fixed here as well:
- tag-only writes to chaptered M4B files failed because FFmpeg copied the chapter data track
- an undecodable cover failed every file instead of falling back to metadata only
- failure reasons stored the FFmpeg command line and absolute paths; they are now sanitized
- size limits used the stored size, so an unknown size bypassed them; writes now check the disk
- atomic replace dropped the file mode and ownership
- the EPUB temp name failed on long file names and could clash with a user file
- tracks in disc folders got duplicate titles
- a PDF worker that exited without a result hung its write forever; it now fails and times out
- null on a NOT NULL library setting returned 500 instead of 400
Generates migrations 0104 and 0105. Every new API field is optional, and released iOS builds
decode none of them.
Ref #694
Refresh dashboard and statistics caches immediately after reading history,
projection, reset, import, and shared deletion changes. Coalesce bulk
notifications while keeping committed changes visible to concurrent reads,
and isolate listener failures from successful writes.
Skip unchanged Hardcover attempt updates and protect cache generations
against stale in-flight loads. Cover date edits, projection-only resumes,
rollback, bulk failures, user scope, and cache races with regression tests.
Ref #1695
Register the identity of delivered bytes so Kobo conversions and audio
removal continue matching their source books in KOReader. Keep download
registration recoverable without allowing database lock waits to stall it.
Preserve manual corrections across cached device identities, make review
semantics explicit, and keep cache invalidation bounded for large libraries.
Add unit, database, lifecycle and scale regressions with one generated migration.
Closes#1697