Reading and OPDS¶
Two ways to read: the built-in web reader, and any OPDS client pointed at the server.
The web reader¶
Open any comic and press Read, or hover a cover in the grid.
| Navigate | click either edge, arrow keys, space, Page Up/Down, Home/End |
| Fit | height, width, whole page, or actual size — remembered per browser |
| Spread | s for two-page spreads, which always start on an even page |
| Direction | d to flip; set automatically from ComicInfo's Manga field |
| Full screen | f |
| Leave | Escape |
Progress saves as you read and the reader resumes there next time; reaching the last page marks the comic read.
Pages are served as URLs, not streamed by the application. The reader asks
/api/comic/{id}/reader for a manifest and fetches the page URLs in it. Pages
extract once into the page cache and are served from there, so turning back a
page costs nothing and a PDF page is rendered once rather than on every view.
That indirection is the point: those URLs can move to a CDN without the reader changing. See Architecture, sections 3A and 3B.
The page cache is capped (LONGBOX_PAGE_CACHE_MB, 2 GB by default) and evicts
whole comics, least-recently-read first, so a half-cached issue never stalls
mid-way. Nothing in it is worth backing up or migrating — it rebuilds on demand.
The reader hardcodes a dark surround regardless of your theme, so artwork is never framed in white.
EPUB is the exception¶
EPUB is reflowable text rather than page images, so the reader turns it away with an explanation and offers the download instead. Rendering its spine documents in an iframe is a separate job, still on the roadmap.
OPDS¶
Longbox serves an OPDS catalogue, so phones and e-readers can browse and download over the network rather than only through this UI.
Both versions of the protocol ship:
| Feed | Version | Format |
|---|---|---|
/opds |
OPDS 1.2 | Atom XML |
/opds/v2 |
OPDS 2.0 | JSON |
The path decides the version. Accept only breaks a tie the path leaves
open: a client naming application/opds+json and not Atom gets JSON from
/opds, one naming both gets the path's answer, and /opds/v2 never degrades —
there is no older version a 2.0 URL could have meant.
What is in the feed¶
/opds is the root, with navigation feeds beneath it:
| Path | |
|---|---|
/opds/comics |
everything, paginated |
/opds/series |
by series |
/opds/publishers |
by publisher |
/opds/years |
by year |
/opds/search.xml |
an OpenSearch description |
Assets sit once under /opds and are shared by both feeds — the file, the
cover, individual pages and the Divina manifest. A CBZ is not a document with a
version, and two URLs for one byte stream would mean two cache entries in every
client and proxy that stores them.
Page streaming¶
Both versions offer the same pages two ways, because readers implement one or the other and rarely both:
- OPDS-PSE — a URL template and a page count, fetched lazily. This is the one that works today: Panels uses it, including for resume.
- A Divina manifest — a document naming every page up front, for Readium-based clients. No reader has opened one of Longbox's yet; Thorium, the obvious candidate, cannot consume a manifest from a remote server. See below.
Signing in from a reader¶
Two ways, and the choice matters on a device with no comfortable keyboard.
HTTP Basic works everywhere and uses your normal username and password — if the account has one.
A token is the better option for e-readers. Generate one on Settings; it
is twelve characters from an alphabet with no I, L, O, 0 or 1 in it,
precisely because it gets typed on a device with arrow keys and an on-screen
grid.
https://comics.example.com/opds?token=XXXXXXXXXXXX
Revoking it on Settings cuts off every device using it at once.
An account that signs in through Google, Apple or Facebook¶
For these the token is not an alternative, it is the only route. Such an account has no password for a reader to send, and a reader cannot perform an OAuth sign-in on its behalf — that needs a browser, and OPDS has nowhere to put one.
Issue a token from the account page and use it exactly as above. Verified 2026-09-22 with a Google Workspace account and Panels on iOS.
Basic auth from such an account is refused with "Incorrect username or password", which is unhelpful on purpose: saying "this account has no password" to an unauthenticated caller would confirm the account exists. The account page says it instead, because it knows who is asking.
Point readers at https:// only
OPDS clients send your credentials with every request. Over plain HTTP that is your password, or a token that grants the same access, in clear text on every page turn.
Token guesses are throttled under the same machinery as failed logins, which is
why LONGBOX_TRUSTED_PROXIES has to be right —
a throttle keyed on a forgeable address is decorative.
Which clients work¶
The rule this project set itself is that the documentation claims no protocol version until a real client has opened it. Two have, against a live deployment, and what follows is what they actually did rather than what the specifications promise.
| Client | Platform | Verified |
|---|---|---|
| Panels | iOS, iPadOS, macOS | OPDS 1.2: browsing, covers, downloads, page streaming, resume, token sign-in |
| Thorium | Windows, macOS, Linux | OPDS 2.0: browsing, facets, covers, metadata, acquisition |
Details, dates and method are in OPDS clients. Both were confirmed by watching the server's access log, not by trusting a user interface — the difference between streaming and downloading is invisible from the outside.
So: OPDS 1.2 and OPDS 2.0 both work with real readers. Page streaming and
resume work. A CBZ, a CBR and a PDF all read over the network, and the CBR is
the interesting one — Panels almost certainly cannot open a .cbr itself, but
streamed page by page it receives ordinary JPEGs, so the container stops
mattering.
What is not claimed¶
Divina manifests. They validate against the published Readium schema and every page they list resolves, but no reader has opened one. Thorium is the obvious candidate and structurally cannot: it has no image-only rendering engine, and it is built for packaged publications rather than manifests streamed from a remote server. Both confirmed by its maintainer. Until a client opens one, this stays unclaimed.
The empty-feed deviation, still open. In a feed with no results the OPDS
2.0 prose requires a key that the published schema's minItems: 1 cannot
accept with zero results. The empty array ships; omitting the key would break
the prose, and inventing a navigation entry would answer a search for nothing
with something. No client has been shown a genuinely empty result yet.
Two things worth knowing before you rely on them¶
Reading position travels one way. Longbox's position reaches a client
through pse:lastRead; the client's never comes back. OPDS-PSE has no
write-back and no ratified OPDS standard covers it — a synchronisation service
was proposed in 2019 and is still a discussion. Panels does sync, with Komga
and Kavita, through each server's proprietary API rather than over OPDS. Read
on one device at a time and nothing is lost.
A cover is not decoration. A comic whose cover could not be extracted carries no image links in the feed, and at least one client hides such entries entirely rather than showing them plain. If a comic is missing from a reader but present in the web UI, check whether it has a cover first.
Clients decode the response, not the declared type. OPDS-PSE permits only
image/jpeg, image/gif and image/png on a stream link, so a WebP or AVIF
comic is advertised as JPEG and served as itself. Panels read all of them
without complaint, which is why Longbox does not transcode pages.
Checking a deployed feed¶
tests/live_check.py walks a deployed server's feed the way a client does.
It is hand-run and not part of the suites, because it is one of only two things
under tests/ that touch the network.
python tests/live_check.py https://comics.example.com -u reader
python tests/live_check.py https://comics.example.com --token XXXXXXXXXXXX
The suites cannot do this job. A TestClient talks to the app object and skips
TLS, the proxy and LONGBOX_PUBLIC_URL entirely — a feed whose links all point
at http://127.0.0.1:8080 validates perfectly and is useless on a phone.