Skip to main content

Downloads and the shelf

A download fetches pages a few at a time, prepares them for the panel on every core, writes each file under a temporary name first, and writes book.json last, so an interrupted download is simply resumed.

Books live on servers: Komga for comics and manga, Mealie for recipes, Calibre-Web for EPUBs and CBZs. A book is read on the reader, often far from Wi-Fi, so it is downloaded first, onto the ShelfShelf: The downloaded books on the reader, one folder each, with book.json written last.. Two constraints shape how. The reader is slow: preparing a page for the panel takes real CPU time, so it is done once, at download time, not at every page turn. And the reader is unreliable: Wi-Fi drops, the battery dies, the reader falls asleep. A download must survive all of that without ever leaving a broken book behind.

A removal company. Movers (fetchers) carry boxes from the old house; packers (preparers) unpack and arrange each room. The inventory sheet is signed last: until it is, the move is not done, and the next crew knows to pick up where the last one stopped.

From a server to the reader​

The app asks every kind of server for books through one Go interface, servers.Server (Three servers, one interface). What matters here is the book's Format: a book of PageImages (Komga's, and Mealie's recipes) is fetched page by page through OpenBook; a CBZ or an EPUB is fetched whole through OpenFile.

The download pipeline​

Tapping a book that is not on the reader calls downloadqueue.EnqueueLocked. Books download one at a time, in the order asked: each is ready as soon as it can be, instead of all of them late. If the queue was idle, the main thread starts the downloader with state.Go, and the downloader calls shelf.Store.Download for each book in turn.

For a book of page images (Komga's), Download runs two groups of workers joined by a small queue:

InteractiveThe download pipeline
Downloading 1 of 12
ServerFetchersQueuePreparers3 pagesnext: 10p9p8p7p6p5p2decodefittoneditherwritep4decodefittoneditherwrite
📁 books/b42/
0001.pg40003.pg4
The bar is the progress the app shows: the pages 1 to n all on disk.
Try this: Press Download. Then try one fetcher: the preparers starve. Six fetchers: the toy server slows down and the queue fills. One preparer: the fetchers wait for room.

The progress the app shows is not "pages done" but "pages 1 to n all on disk": a page that finishes early waits until every page before it is there (finishPage). That is the number of pages you could read right now.

A CBZ goes through the same workers, but its file is fetched whole first and its pages taken out of it; the archive is deleted once the pages are prepared. A ReflowableReflowable: A book whose text is laid out again for the screen and font size, like a web page, as opposed to fixed layout. EPUB is fetched whole and kept as book.epub: it is read through once to build its index.json, its cover becomes cover.pg4, and its pages are laid out later, while reading.

Never half a file​

A reader can die at any instant. Two rules make that harmless.

Every file is written under a temporary name, then renamed. writeFileAtomic writes 0007.pg4.tmp, then renames it 0007.pg4. A rename is an Atomic renameAtomic rename: Write a file under a temporary name, then rename it: other code sees the old file or the whole new one, never half.: the file under its real name is either absent or whole. A .tmp cut short by a crash has a name no code reads. A CBZ or an EPUB is streamed to book.cbz.part or book.epub.part the same way, and renamed once every byte is in.

book.json is written last. It holds the book's titles, page count, format and progress. Store.Book reads it to decide whether a book is downloaded. So a folder without it is, by definition, an interrupted download. The next Download of that book picks it up: the fetchers check pageOnDisk and skip every page already there.

InteractivePull the plug
⚡ Unplugged
📁 books/b42/
0001.pg40002.pg40003.pg40004.pg4.tmp
Store.Book("b42") → false: no book.json, so not downloaded

The reader died in the middle of this download: whole pages, a .tmp cut short, no book.json. Tap to download again, or start a new download and pull the plug yourself.

Try this: Tap to download again: the blue pages are skipped, the .tmp is written again. Then start a new download and pull the plug yourself. Switch to the EPUB and make the Wi-Fi drop.

The EPUB tab shows the difference between the two failures. Pulling the plug leaves book.epub.part; the next download creates it again, empty, and starts over (a partial EPUB is never resumed, only a whole book.epub is kept). Wi-Fi dropping is different: the request would wait forever, so a watchdogWatchdog: A timer that gives up on a download when no data has arrived for 30 seconds. gives up after 30 s without a byte (stallTimeout), and the download fails with "the download stalled", removing the .part.

How Inkshelf does it​

  • downloadqueue.downloader calls Store.Download with a progress callback. The callback is an Update whose change, noteProgressLocked, asks for a redraw only on the screens that show it, for the first page, then at most every 2 s (progressRepaint): each redraw takes the CPU from the download.
  • Download passes progress through a reporter: a goroutine of its own and a channel that keeps only the latest value, so a slow callback never holds up the workers.
  • While downloading, the reader is kept awake: pocketbook.PreventSleepFor(awakeFor), one minute, renewed every 30 s.
  • After a book, the downloader saves its cover for offline use and, inside one Update, records the result and takes the next book from the queue.
Going deeper
  • The first error stops all. finishPage records the first error and cancels the context, which stops the other workers; the preparers drain the queue so that no fetcher stays blocked.
  • Old books. pageOnDisk also accepts 0001.png: books downloaded by older versions keep working.
  • Fixed-layout EPUBs, whose pages are drawn like pictures, are drawn page by page into .pg4 files at download time and kept as a book of pages.
  • The HTTP client. httpclient.New makes the one client every request goes through, with a one-minute timeout; Calibre-Web downloads files through a copy without that timeout, which is why files need the watchdog.
  • Cancelling. Tapping the book being downloaded cancels its context; the list then says "Cancelled". Whatever was written stays, ready to resume.

Where it lives in the code​

The full story: Files on the reader.

Next: once a book is on the reader and you read it, your progress has to go back to the server: Syncing reading progress.

Check yourself​

The reader dies while writing page 7. What does the folder hold afterwards?
Why are pages prepared at download time rather than when they are read?
An EPUB download stops receiving bytes. What happens?