A desk for your ebook shelf.
Search Anna's Archive, download what you pick, give the files sensible names, file them into author and series folders, keep a copy in Dropbox, send them to any device running KOReader, and push what you have read back to Goodreads.
Windows · MIT licensed · runs entirely on your own computer
It runs in a window of its own. Nothing runs on anyone else's server: the app is a small
web page served to itself on 127.0.0.1:8765, and your keys stay in a config.json file
beside the program.
Shelfwright hosts no books and ships with no content. It automates the same steps you would otherwise do by hand in a browser, using your own accounts.
Contents - Please buy the books · Installing · How it works · Under the hood · Supporting this · Disclaimer · Building it yourself · Your data · Issues and contributions · License
Shelfwright makes it trivially easy to acquire more books than you will ever read. That is the one thing wrong with it.
Someone spent a year of their life - sometimes five - on the file you just got in four seconds, and for most writers whether they get to do it again at all depends on whether the last one sold. A download is not a sale. It is not a review, a library request, or a book pressed into a friend's hands either.
So use this for what an archive is genuinely good at: the out of print, the untranslated, the book no shop within a hundred miles stocks, the twelfth in a series nobody is reprinting. And when one turns out to matter to you - buy it. New, where the writer is actually paid. Ask your library to order it when you cannot. Buy it for somebody else.
An archive keeps books alive after the fact. It does not pay anyone to write the next one. If a book was worth reading, the person who wrote it is worth paying, and a shelf you can hand to someone is worth having.
What you need
- Windows 10 or 11. There is no macOS or Linux build.
- Microsoft Edge, which Windows ships with. It is never opened in front of you, but the free downloads and the Goodreads features drive it - see Under the hood.
- WebView2, which Windows 11 and current Windows 10 also ship with. Without it the app opens in your ordinary browser instead of its own window and works exactly the same.
Nothing else. No Python, no runtime, no account.
⬇ Download the latest installer - grab
ShelfwrightSetup.exe from the newest release and run it.
Windows will warn you that it is from an unknown publisher, because it is not code-signed (a certificate costs a few hundred a year). Click More info → Run anyway.
The installer asks where to put the app - %LOCALAPPDATA%\Programs\Shelfwright unless
you change it - and offers two tick boxes, both on by default: a Desktop shortcut, and
starting the app when it finishes. There is also a Bring settings… box: point it at a
config.json from another machine and you arrive with everything already set up.
Nothing goes in the registry and there is no uninstaller - deleting the folder removes the program, its settings and its downloads. (The Desktop shortcut lives outside that folder, so delete it separately.)
Everything else lives in that one folder: the program, your settings, your queue, the log, and unless you choose otherwise your downloads. Copy the folder to another computer and everything comes with it.
First time you run it, a nine-step walkthrough explains the basics, then Settings opens so you can fill in what you want to use. You do not need an account - without one, books come down the free route instead.
Type a title, an author, or an ISBN. Filter by type and language, pick a sort order, and choose how many results to show at a time. Save to decides where the download lands - this computer, your cloud library, or both.
The header pill tells you where you stand at a glance: the domain in use, whether you are signed in, and whether the next download will be fast or slow.
Type and Language are both tick lists, not single choices. Ask for EPUB or PDF, English or Italian, and you get everything that matches any of them - which is how you find the one edition that exists in your language without running the search four times. The button wears what you have picked as chips - two of them, and then a count for the rest, so the row does not shuffle about as you tick.
The language list runs to forty-odd, from Italiano and Română through to 日本語, so it has
a box at the top to narrow it down. Type either name - italian finds Italiano, and
german finds Deutsch. Clear puts a filter back to "any".
Everything picked here applies to the List of titles tab too.
A search brings back everything the archive will give for it - fifty hits for a common title, rather than the first five - and shows them a page at a time. Results per page sets how big a page is; the numbered buttons under the last card turn to the next one, and the header says which slice you are looking at.
Turning a page does not search again. The whole set is held for as long as it is on screen, so paging back and forth is instant and costs the site nothing. Pressing Search again is what asks it afresh. When there are pages, ⬇ Download all becomes ⬇ Download this page, so it queues what you can see rather than fifty books at once.
Why a search is sometimes instant and sometimes takes half a minute. Shelfwright always tries an ordinary request first. When the site's anti-bot protection turns that away - a 403 or a 503, demanding the visitor prove they are a real browser by running some JavaScript - the same page is fetched through a real browser instead, which takes twenty seconds or so.
That branch is decided by the site's answer, not by whether you have a key. Having a membership makes it far less likely you are challenged, but a signed-in search can still end up on the slow road, and a keyless one can sail straight through.
Every card carries the cover, the format, the size, the language, the year, and how many people have downloaded that particular copy. That last number is usually the best guide to which of five near-identical files is the good one. A ★ count is the archive's quality rating; a red ⚠ is the number of reported problems with the file.
Cards also tell you what you already have - ✓ already downloaded if the file is in your
download folder, ☁ in your library if it is in Dropbox - matched loosely enough that
punctuation, capitalisation and a (2) suffix do not fool it. The grey hash at the end
is the record's md5; click it to copy.
Download fetches one book. + Add to queue collects them, and ⬇ Download all queues everything on screen and starts - skipping anything already on disk. The queue holds 15 books at a time and survives closing the app; books can be removed from it or cleared, but not reordered.
There are two ways a book can arrive, and Shelfwright picks for you.
Fast downloads. If you have an Anna's Archive membership, your key buys a number of fast downloads. These come straight down over an ordinary connection: quick, no fuss. The header shows how many you have left, read live off your account page rather than guessed
- the site reports downloads used over a rolling window, so the figure shown is that subtraction.
Slow downloads. With no key, or once the allowance is spent, Shelfwright uses the free mirrors instead - the same ones you would use by hand: open the book's page, click through to a partner server, wait out its countdown, then download.
This happens automatically. Running out of fast credits does not stop a queue or ask you anything; it carries on the slow way and says so. Slow downloads take a few minutes per book because of the countdown, and occasionally the mirror asks for a captcha.
The List of titles tab takes one title per line and looks each one up, keeping the most-downloaded match for each. Results appear as they arrive rather than all at the end, with a count of how many are still to come.
Because the most-downloaded copy is not always the right one - a wrong edition, a bad scan - each result keeps its runners-up. Press ⇄ other matches on a card to see the alternatives as full cards, same covers and same counts, and swap. A list is one card per title and has nothing to page through, so the number in the filter row becomes Number of alternatives here: how many runners-up each title keeps. Set it to 10 if you want plenty to choose from.
Files are named Author - Title.epub, and with the AI naming pass on, a book in a series
becomes Author - Series 04 - Title.epub.
In your download folder they sit flat, all in one place. In the cloud library and on
your reader they are filed into folders - author at the top, series inside that - so a
shelf of forty books stays navigable. The filename keeps the whole name either way,
because a file called 04 - The Alloy of Law.epub is only meaningful while it stays put,
and files do not stay put.
Book files often arrive named Lord of chaos 6.epub. Shelfwright can ask Google's free
Gemini service to work out the real title, series and number, so that becomes
Robert Jordan - The Wheel of Time 06 - Lord of Chaos.epub.
It needs a free Google API key (Settings → Names). Only text about the book is sent -
title, author, publisher, the original filename and the record's id - never your keys and
never the file itself. The free tier is counted per model, so when one runs out
Shelfwright moves to the next rather than giving up. If it fails or you leave it off,
files keep their ordinary Author - Title names.
Connect Dropbox in Settings and the Library tab becomes a folder browser over your collection, with download and delete on every book, and folder deletion that tells you how many books go with it. Books saved here are reachable from any computer.
Connecting takes one step you do on Dropbox's own site: create an app at dropbox.com/developers/apps, choose Scoped access → App folder, and paste its App key into Settings. Shelfwright then hands you a link to approve and a code to paste back - no password, and no local server listening for a redirect.
Choosing App folder is what confines it: Dropbox gives the app its own folder and it can see nothing else in your account. Uploads never overwrite.
If you read with KOReader - on a Kindle, a Kobo, a PocketBook, anything it runs on - Shelfwright can talk to your device over your WiFi - no cable. In KOReader open Network → SSH server and start it; it shows the address and port to put in Settings.
The KOReader tab then browses your device like the library. It shows each book's
reading status - finished, in progress with a percentage, on hold, or not started - read
from KOReader's own sidecar files. Each folder carries a running tally beside it
(✓ 8 ◐ 0 ○ 0 above): finished, in progress, and not yet started, with on-hold books
counted among the last.
Compare with library shows what is on one side and not the other, and copies in either direction in one go. Nothing is ever deleted by a sync; a missing book only ever means copy.
You can also set a book's status from the app: mark it finished, put it on hold, or reset it entirely. KOReader only notices these changes after it restarts.
Every started book in the KOReader tab has a 📖 button. Press it and Shelfwright finds the book on Goodreads, shows you what it will do, and - once you confirm - moves it to Currently Reading or Read, records how far through you are, and takes your rating.
Nothing has been written at the point in that picture. It has found the book, worked out what needs to change, and is waiting.
It asks which edition a book is only once and remembers the answer. Matching deliberately pushes down summaries, study guides, boxed sets and dramatised adaptations: they carry the real book's title and would otherwise score perfectly. Nothing is written until you press the button, and nothing is ever removed from your shelves.
The first time, a browser window appears asking you to sign in to Goodreads. Sign in with an email address and password, or with Amazon.
"Continue with Google" will not work. Google refuses to sign in from a browser it detects as automated, and shows "Couldn't sign you in - this browser or app may not be secure." That is a deliberate protection on Google's side and Shelfwright does not try to get around it. If your Goodreads account was created through Google, use Forgot password on Goodreads once to set a password, and sign in with that from then on.
You are asked once. The sign-in is remembered, and afterwards everything happens with no window at all.
Six tabs: Account (domain and secret key), Downloads (where files go), Names (the AI tidying), Library (Dropbox), KOReader (your device), and Advanced (developer mode).
Some things save the moment you change them - the fast-downloads switch, developer mode, the Save to destination, and the download folder, which is why that box is read-only and changed with Browse… rather than typed into. The rest need the Save settings button, and the KOReader page is written by Test connection, so you find out it works at the same moment it is stored.
Both collection tabs are always visible. If something is not set up yet, the tab explains what it would do and offers to take you to the right settings - and a thing that is set up but unreachable says so plainly, which is a different problem with a different answer.
Skip this unless you are curious - none of it is needed to use the app.
It is a local web app wearing a native frame. Flask serves the page on
127.0.0.1:8765 (or any free port if that one is taken) and a WebView2 window points at
it. Flask runs on a daemon thread, because the GUI toolkit insists on owning the main one,
and the launcher waits for the socket to answer before opening the window.
The browser it drives is real - it is your own Microsoft Edge - and parked off the edge
of the screen. Both the free downloads and the Goodreads features need a browser, because
the sites involved put a JavaScript challenge in front of anything else. That was measured
rather than assumed: even a Python client impersonating Chrome's exact TLS fingerprint is
refused, because the challenge has to actually run. Nothing is downloaded to satisfy
this; Playwright is pointed at the Edge that ships with Windows. Playwright fixes headless-or-not at launch, so a browser that might
need to be shown later can never start hidden - it starts visible and gets moved to
(-32000, -32000) instead. The window comes back into view for three reasons: a captcha
during a slow download, the first Goodreads sign-in, and a site check that could not be
satisfied on its own. If you would rather watch it work, WATCH_BROWSER = True at the top
of slow_lib.py leaves it on screen.
The browser only finds the address; requests moves the bytes, carrying the browser's
cookies into a resumable .part file - which frees the browser immediately for the next
book. Exactly one browser runs at a time, shared by downloads, walled searches and
Goodreads: a browser profile can only be opened once, and parallel searches used to lock
each other out and come back empty, which looked exactly like "no match for that title".
Parsing the archive takes three non-obvious moves. Half the result rows ship inside
HTML comments and are un-commented client-side, so the delimiters are stripped first. Row
boundaries are found by climbing from each /md5/ link until an ancestor would swallow a
different record - never by CSS class, which changes on every restyle. And the download
counters are not in the search HTML at all; they are fetched per record from the endpoint
the site's own page uses.
Paging is done here, not there. One search of the archive answers with a page of about
fifty hits, and a multi-valued filter is repeated parameters rather than a comma-joined
one - ext=epub&ext=pdf is how the site's own checkboxes ask for "either". The whole
answer is kept server-side against the query it came from, and the page you are on is a
slice of that, which is why turning a page is instant. It also decides how many of those
per-record counter lookups happen: only the slice on screen is filled in, and remembered,
so a fifty-hit search does not fire fifty extra requests to show you ten books.
Covers are proxied through 127.0.0.1. Edge's tracking prevention inside WebView2
blocks third-party image hosts before the page ever sees the request, so every cover fell
back to a placeholder while the identical URL fetched perfectly from Python. The proxy is
deliberately narrow - https only, image/* only, 4 MB cap - with a known-mirror fallback
for cover hosts that answer a plain request with a "checking your browser" page.
Frozen builds split two ways on purpose. The HTML template ships inside the .exe and
is read from PyInstaller's temp unpack directory, which is wiped on exit. Everything that
must survive a restart - config.json, queue.json, app.log, downloads/, the browser
profiles - resolves to the folder holding the .exe instead.
The integrations are scoped so a bug cannot be catastrophic. Dropbox is App Folder only, and its sign-in uses PKCE with a pasted code, so there is no local server and no port to open. Uploads never overwrite and syncs never delete. KOReader sidecars are edited in place and re-parsed before upload, because the same file holds every highlight and bookmark for that book. The Goodreads sign-in happens in a real window you drive yourself
- the app never sees the password - and a weak edition match is refused rather than guessed, because a wrong guess lands on a public profile.
| Module | What it is |
|---|---|
webapp.py |
the Flask server and every API route - the app itself |
annas_downloader.py |
searching, parsing, naming, and fast downloads |
slow_lib.py |
the off-screen browser: free downloads and walled pages |
dropbox_lib.py |
the cloud library |
koreader_lib.py |
SSH/SFTP to KOReader, and its reading-progress sidecars |
goodreads_lib.py |
shelf and rating updates, and edition matching |
title_ai.py |
the optional Gemini naming pass |
desktop_lib.py |
the native window |
templates/index.html |
the entire front end, in one file |
Shelfwright does nothing on its own. Every book it finds comes from Anna's Archive, and every byte it moves costs them servers, storage and bandwidth. If this app is useful to you, that is because they are useful to you - so send the money there.
Their donation page is /donate on any current official mirror.
A membership also buys the fast-download credits Shelfwright shows in its header, so it is
not charity in one direction.
Warning
Check the FAQ before you pay anyone. Anna's Archive changes domains often, and the
project publicly names several lookalikes as impostors that steal donations. At the time
of writing their official mirrors are annas-archive.gl, annas-archive.pk and
annas-archive.gd, and annas-archive.su, .io and .is are listed by the project
itself as fraudulent. Take the live list from
their FAQ rather than trusting a link in this README.
That one is at the top of this file, where it belongs.
This project is not affiliated with, endorsed by, or connected to Anna's Archive.
It is a personal open-source project that talks to their public site from my own computer, the same way a browser does. Nobody there has reviewed it, nobody there is aware of it, and any bug in it is mine - not theirs. Do not report Shelfwright's problems to them.
Shelfwright hosts, stores and distributes nothing. You bring your own accounts, and what you do with them is your business and your responsibility, under whatever laws apply where you are.
I do not condone or encourage copyright infringement. This is a client: it automates clicks a person could make in a browser, against a site it does not run. Whether a given book is lawful for you to download depends on the book and on where you live, and that judgement is yours to make - which is the other half of why the top of this file asks you to buy the ones that matter.
Written by a professional senior developer, with heavy use of AI coding tools. The architecture, the decisions and the review are mine; a good deal of the typing was not.
python -m pip install -r requirements.txt pyinstaller playwright
python webapp.py # run from source
python build.py --setup # -> dist/Shelfwright.exe AND dist/ShelfwrightSetup.exeThere is no playwright install step. Playwright is used only to drive the copy of
Microsoft Edge already on the machine, so the browsers it would otherwise download are
several hundred megabytes of nothing.
That is the whole release build. The two halves can still be run on their own -
python build.py for just the app, python build_installer.py to wrap an app that is
already built - but --setup does both in one go, and stops before wrapping if the app
build failed, so the installer can never quietly hand out the previous .exe.
Important
What ends up inside the .exe is whatever was installed when you built it.
PyInstaller can only bundle what it can import, so building from the wrong interpreter
produces an app that is quietly missing pieces.
flask, requests and beautifulsoup4 are fatal - without them the app exits on its
first line, and because the build is windowed there is no console to say so: it just
appears not to start. build.py checks for those three and refuses to build rather than
hand you an .exe that cannot open.
playwright, pywebview and paramiko each cost a feature instead: no browser engine
means no free downloads, no Goodreads and no searching at all when the site puts up its
wall; no pywebview means the app opens in a browser rather than its own window; no
paramiko means no KOReader. The build names each one it could not find and carries on -
read those ! lines, not just the last line.
The .exe is self-contained: Python, Flask, the HTML page and the window toolkit are all
inside it. Nothing secret is baked in - config.json is read at runtime from the folder
the program sits in.
Everything is kept in the app's own folder, beside the .exe:
| File | What it holds |
|---|---|
config.json |
your keys and logins - never share this |
queue.json |
what is waiting to download |
goodreads.json |
which Goodreads book each of yours is |
browser-profile/, goodreads-profile/ |
the browsers' saved sessions |
app.log |
what happened, for when something goes wrong |
app.log only appears in the packaged app, which has no console to print to. Run from
source and it prints to your terminal instead.
Everything stays on your computer. The only things sent anywhere are your searches and downloads (to Anna's Archive), your books (to Dropbox and your reader, if you set those up), your shelf updates (to Goodreads, if you use it), and book titles (to Google, if you turn on name tidying).
config.json is plain text. Anyone with your user account on this machine can read the
keys in it, and it is the one file never to copy anywhere.
This is a hobby project maintained by one person in their spare time. Nothing here is promised, and there is no support commitment.
Bug reports are welcome, and the useful ones say which version you are on, what you
were doing, and what the relevant lines of app.log say - read them first and take out
anything personal, because that file records what you searched for. Expect the commonest
report by far to be "search suddenly returns nothing": the archive restyles its pages
from time to time and the parser needs a nudge when it does.
Pull requests are welcome but not guaranteed a merge. Open an issue before writing anything substantial, so nobody spends an evening on a direction I was not going to take.
Out of scope: finding or requesting particular books, anything about how Anna's Archive itself works, and anything that needs an account I would have to hold.
For a security problem, use GitHub's private vulnerability reporting on this repository rather than a public issue.
MIT - see LICENSE. Do what you like with it.
The packaged .exe bundles Flask, requests, BeautifulSoup, paramiko and pywebview, each
under its own licence. Paramiko is LGPL-2.1 and is linked statically by PyInstaller; the
complete source for this program is the repository you are reading, and rebuilding it with
a modified paramiko is python build.py --setup away.











