The web audiobook player shared the reading session composable, which never sent a
sessionType, so the server stored every listening session as a read session and counted
audiobook time as reading in activity stats. Let callers pass an optional sessionType and
have the audiobook view send listen. Other readers keep omitting the field, so their
payloads are unchanged. Existing rows are not backfilled.
Let users hide request entry points and notifications while existing requests continue running. Keep the setting optional so older clients retain their current behavior.
Ref #1476
Allow 0.05% adjustments while preserving existing integer settings and the 98% default. Keep finish detection stable when progress is stored as PostgreSQL real.
* fix(koreader): respect KOReader global HTTP proxy settings
Allow KOReader's Network -> Proxy settings (stored in G_reader_settings)
to be respected for both JSON API requests and binary downloads.
Also support explicit proxy in BookOrbitApi options, and route Tailscale
tailnet addresses (100.x.y.z and *.ts.net) through the local userspace
HTTP proxy (127.0.0.1:1056) when running on e-ink devices lacking TUN.
Fixes#753
* fix(koreader): address review feedback on host parsing and HTTPS proxy safety
* fix(koreader): simplify getProxy to respect KOReader proxy settings without vendor fallbacks
* fix: tunnel KOReader HTTPS through configured HTTP proxies
Route HTTPS requests through CONNECT so proxy-only servers stay reachable without exposing BookOrbit credentials to the proxy. Restrict redirects to the original host and allow secure upgrades for API calls and downloads. Bump the plugin version so installed clients discover the fix.
---------
Co-authored-by: Andrej <acikarski14@gmail.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Use human activity and observed head changes because GitHub can update PR timestamps without contributor activity.
Audit every open PR before applying changes and start existing PRs from a safe baseline.
* fix(metadata): keep a pinned hardcover edition past the edition cap
resolveCachedMatch looked the cached edition up in the capped 50-edition list of the work, so on
works with more editions (Harry Potter, Le Petit Prince, Dune) a manually chosen edition past the
cap was never found and pickBestEdition silently re-pointed the book on the very sync that
setEdition triggers. Resolve the cached edition by id, scoped to the book, and only fall back to
the capped list when Hardcover no longer has that edition on the book.
Fixes#1252
* fix(metadata): serialize Hardcover edition changes with sync
Keep a manual edition choice from being overwritten by an older in-flight sync. Queue sync and edition operations per user and book so the manual choice runs after earlier work.
Ref #1252
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
* fix(metadata): write finalized metadata back to the imported file
Finalizing a Book Dock file only moved the bytes, so a library with file
writes enabled kept the metadata the file was uploaded with rather than
the metadata the dock settled on. Schedule a write-back for every book a
finalize produces once all of them have their metadata, attributed to
the uploader.
Fixes#1385
* fix(metadata): schedule dock file writes after cleanup
Prevent a failed Book Dock cleanup from leaving a pending file write. Keep the integration assertion synchronized with the debounced writer and restore the missing directory stability mock for end-to-end coverage.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
* fix(auth): recover from expired edge auth proxy sessions
The service worker answered every page load from the precached app shell, so once an edge auth
proxy's session cookie (e.g. Cloudflare Access, OAuth2 Proxy) expired, the proxy never saw the
navigation and could not redirect to its login page. The app loaded from cache, its API calls were
redirected cross-origin and failed, and it fell back to its own login screen with no way out short
of clearing site data.
Serve navigations network-first and fall back to the precached shell only when offline.
vite-plugin-pwa defaults navigateFallback to index.html, which would register a cache-only route
ahead of this one, so disable it explicitly. Cache only 200 responses so a proxy's redirect is never
stored as the page.
Request API redirects manually so a proxy redirect is detected directly and triggers a full reload,
which the proxy can then send to its login page. Cap these reloads to one per 15 seconds so a reload
answered from cache again surfaces an error instead of looping.
Fixes#642
* fix(auth): harden edge auth proxy recovery against loops and cache growth
Force manual redirects after caller options so no request can opt back into following a proxy
redirect. Skip the reload when session storage is unavailable, since the loop guard cannot work
without it. Drop the navigation network timeout so a slow proxy redirect is never replaced by the
cached page, and cap the app shell cache, which stores one entry per visited URL.
Ref #642
* test(auth): isolate useLoginOptions module state between tests
The composable keeps its cached options, error, and in-flight request at module level, so they
carried over between tests in the file. Reset the module and import it fresh for each test so a
future test cannot pass or fail depending on what ran before it.
Ref #642
* fix(auth): let edge proxy handle sign-in navigations
Keep the offline shell under a distinct precache URL so Workbox cannot serve
root navigations before the proxy sees them. Route public auth requests through
the redirect-aware fetch path so expired edge sessions open reauthentication.
Ref #642
* fix(auth): keep offline shell off root URLs
Prevent Workbox from mapping root query variants to the precached offline shell so expired proxy sessions reach the network.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Refresh the web access cookie after a media load failure and resume from the last observed position. Limit retries so unrelated media errors surface promptly.
Closes#1536
Match subtitle metadata in book searches and index it for large libraries. Add regression coverage for accent matching and library visibility.
+ add open collective
Closes#1552
Add a reusable pagination control and cover Book Dock navigation with focused tests. Align the client PDF dependency and lockfile for clean CI verification.
Use the browser origin when APP_URL points to a local address so setup links work through a public proxy. Preserve public canonical URLs and HTTPS.
Closes#1548
A file filed into a book that already existed was added without re-ranking, so the
book kept whichever file created it: an ebook that arrived first stayed primary in a
library that ranks audiobooks first. Imports and uploads now re-rank with the
library's priority, keep the current primary on a tie, and a rolled-back finalize
restores the primary it replaced.
Every format chip, edition row and format menu now builds from shared helpers: content files
only, a read-along EPUB kept apart from a plain one, in the library's format priority, which
gains a read-along entry. Labels, colours and the chip each have one source, and cards, OPDS,
email, Kobo and the dashboard rank files the same way. Book detail file order is unchanged
because iOS builds its track queue from it.
The plugin Set up form reused the new-source editor, which always showed
an Indexer type select listing only torznab and newznab. Changing it
saved a torznab feed under the plugin's name, which is how a MyAnonaMouse
feed ended up pointed at the tracker's website.
"Read from here" and the page start resolved the range to the nearest element with any id and
matched SMIL entries against that id. Storyteller keeps the book's inline elements inside its
sentence spans with their ids intact (Kobo's kobo.N.N, calibre's calibre_link-N), so in those
books the nearest id named no entry and narration restarted at the top of the chapter, while
playback kept highlighting the same sentences correctly.
Start at the first SMIL entry whose element overlaps the range instead, looking each entry up the
way foliate resolves the highlight. Foliate already walks entries in reading order through the
start filter, so the vendored engine is unchanged.
Fixes#1528
Evaluate identified KOReader clients independently so a stale sibling device does not suppress safe updates. Keep the conservative account-wide gate for legacy or unusable client identities.
Fixes#1519
Keep one cover slot per medium and show the one each surface needs: the
library's shape for the face, square art on audio screens, the ebook slot
for Kobo and OPDS. Legacy covers convert on upgrade or on first read, and a
write that names no slot keeps working for released iOS builds. Also read
the dev client origin from one constant.
Run the dev API on 6262 and Vite on 6263 with strictPort; production
defaults stay on 3000. Define the dev client origin once and read the
typed appConfig instead of repeating its fallback in every gateway.
Load read-aloud voices only once playback starts, so the app-wide player
no longer fires a 401 on public pages. Let the session-failure handler
clear auth without leaving a public route, which also keeps magic links
and the sign-in redirect intact.
Fixes#1503
Carry positions between sibling EPUBs even without an audiobook, and keep
narration markers on the read-along file only. Point the KOReader book panel
and hold release at the copy devices sync, and label EPUB-only sync in the
read-aloud panel.
Fixes#1508
Track colliding KOReader statistics rows independently and require verified explicit targets before upload. Keep unresolved history pending with a visible recovery path.
Fixes#1457
Accept android as a reading-session source from clients and report it as
its own statistics bucket, labelled Android app, beside the iOS app and
Apple Watch. A generated migration adds it to the reading_sessions check
constraint, achievements count Android reading toward Full Orbit and Two
Worlds, and the web charts draw it in a new --pill-android token at hue 95.
Recompute the open file digest and recover previously verified matches without guessing between duplicate files. Preserve stranded statistics across lifecycle retries and advertise plugin version 1.5.3 so installed devices can update.
Fixes#1497
Request cached tags for ISBN and stored-ID GraphQL lookups. Normalize the Genre bucket so identified books retain the same genre metadata as free-text search results.
Fixes#1422
Wait for the requested SMIL fragment to resolve before falling back, and invalidate stale audio loads so overlapping starts cannot orphan playback.
Fixes#1477
Keep pagination and the selection popup idle until native selection gestures finish, including cancelled touches.
Cover paginated and scrolled flows while retaining ordinary swipe snapping.
Closes#1487
Block text was unescaped from a six-entry table, so only &, <, >, ",
' and ever became characters. Every numeric reference survived into the
text handed to the TTS provider, and an EPUB that writes its apostrophes as ' had
them read out as "hash thirty-nine" while the page showed a normal apostrophe. Which
apostrophes broke depended only on how the book was serialized: ' was in the
table, ' was not, and both appear in the same sentence often enough.
Parse each block instead of stripping tags and unescaping by hand. The parser decodes
whatever the reader's own DOM decodes, which is the property that matters here because
the narration is read against the page, and it decodes in one pass, so a book that
quotes &#39; is still narrated as '.
Only the text conversion changed. Block boundaries still come from the same split on
block-element opening tags, so block indices stay aligned with Foliate's highlight
blocks and audio cannot drift a paragraph away from the highlight.
Do not route this through htmlToPlainText. That helper strips tag-shaped text a second
time after decoding, which is right for the scraped metadata it was written for but
silences book content: a code sample written <div>hi</div> would narrate as
"hi", and "5 < 6" would lose its operator. Tests cover both.
Fixes#1486
A cover refresh skipped thumbnail generation whenever the stored cover
source was custom, so re-extract reported success while /thumbnail kept
404ing. Generate the missing thumbnail from the custom cover, and repair
any other gap lazily on the first thumbnail request.
Fixes#1475
The new assertions read `apiMock.mock.calls[0][1].body` straight off an `unknown`, which vitest
runs happily and `vue-tsc` rejects. Read them through one typed helper instead.
Every settings write from the reader PUT the whole object: the defaults write sent all twenty-one
EPUB fields and the per-book write sent the entire accumulated delta. PUT replaces the stored row,
so a write from this client threw away whatever another one had set since the page loaded.
That is a real conflict now rather than a theoretical one. The iOS reader owns the look fields and
keeps the layout fields on the device, so it writes a small subset and leaves the rest alone. A
full snapshot from a web tab reverted its look edits silently, and there was nothing either side
could do about it: last writer won the whole row.
Move the three writes to the PATCH routes, which merge field by field on the server, so two
clients editing different fields no longer race. An empty patch is dropped rather than sent,
because the server rejects an empty body.
Reset still deletes, on both routes: removing a customization is a whole-row operation and means
the same thing to every client.
PUT reader/defaults/:formatGroup demands the full settings object, so a client has to send every
field of a group to change one of them. That forces a client to own fields it does not model and to
echo back values it never shows, and it turns two clients editing different fields into a
last-writer-wins race over the whole row.
Add PATCH for both defaults and per-book preferences. The defaults patch merges over the stored
row with the built-in defaults folded in underneath, so the row stays a complete object for the web
client's sanitiser while a partial caller leaves no holes. The per-book patch takes set and unset
and resolves both in one statement, so a concurrent patch cannot lose a field to a read-modify-write,
and deletes the row once its last key goes, which is the same state as never having been customized.
Nulls are never stripped: a null fontFamily means "use the book's own font", not an absent key.
Keep only the keys the caller actually sent. Zod applies .default() while parsing a partial, so a
cbx patch that never mentioned spreadGap came back carrying one; the existing per-book PUT had the
same latent bug and now shares the fix, which stops a touched comic from being pinned to the
default gap.
iOS is the only caller of the new routes. It keeps the layout fields on the device and syncs the
look fields, which is exactly the split a partial write makes expressible. The web client does not
call them and still PUTs full snapshots.
The activity overview derived completions from reading sessions that reached 99 percent, while the
dashboard's reading-goal widget counted completed reading attempts. The two disagreed on iOS, where
Home reads the widget and the Annual Goal card reads the overview: a book marked read by hand, one
finished on Kobo or KOReader, an audiobook whose last session stops short of the end, and a re-read
collapsed into its first finish were all invisible to the session heuristic.
Commit 3de365a0 already made completed attempts canonical for the web timeline, goal trajectory and
completion latency. The activity-overview route arrived later with its own session-based query and
reintroduced the divergence on the one surface the web never calls.
Drop the timezone conversion with it. endedOn is a calendar date rather than an instant, so it has
no zone to convert and AT TIME ZONE would only shift buckets across year boundaries.
Fixes#1474
Custom releaseRules short-circuit semantic-release's default rules, so a
feat carrying a BREAKING CHANGE footer resolved to minor. v2.11.0 should
have been v3.0.0.
Replace the hardcoded English in the TTS and narration components with
message keys, label every icon-only control, and add settings.common.nav.tts,
which the settings sidebar rendered as a raw key.
Stop requiring PODCAST_ENCRYPTION_KEY while APP_FEATURES.podcasts is off, and
skip podcast routes and permissions in the authorization matrix for the same
reason.
Pass the direct-download settle() helper the SETTLE_MS budget the file already
defines: vi.waitFor's one-second default cannot cover six DNS-resolved redirect
hops on a cold CI resolver.
Add a short section pointing at bookorbit.app/my-workflow, the read-along
EPUB3 setup used day to day across iPhone, Apple Watch, KOReader, and the
web reader.