# TomeSync KOReader Plugin Syncs reading progress and sessions between KOReader and your Tome library. --- ## Setup 1. Open Tome in your browser at `http://:/settings` 2. Scroll to the **KOReader** section 3. Click **Download Plugin** 4. Extract the ZIP to `koreader/plugins/` on your device so you have `koreader/plugins/tomesync.koplugin/main.lua` 5. Restart KOReader The plugin is pre-configured with your server URL and API key. No manual configuration needed. **Important:** Download the plugin from the same address your e-reader can reach. If your server is at `:8080`, open the settings page at that address before downloading. --- ## Features - **Reading sync** -- position, progress, and reading sessions sync between KOReader and Tome's web reader - **Rating sync** -- KOReader's native star rating and review sync both ways with Tome - **Reading-history import** -- backfill Tome's Stats with KOReader's own per-page reading history from before TomeSync (reading time and pages only -- never your read/unread status) - **Series browser** -- browse your library's series and download entire series to your device in one tap - **Offline-safe** -- everything works seamlessly when your server is unreachable; sessions queue and flush later - **Context-aware menu** -- different options when a book is open vs. from the home screen --- ## How It Works ### Book Matching When you open a book, the plugin tries to match the file to a book in your Tome library: 1. Checks its local cache for a known mapping 2. If not cached, sends the filename to `GET /api/tome-sync/resolve?filename=` 3. The server matches by file path, title, and volume number (e.g. `Vol. 1` in the filename matches `series_index = 1`) 4. Once resolved, the mapping is cached locally so future opens are instant Books downloaded via OPDS are matched automatically. Books sideloaded from other sources will also match as long as the filename contains the book title. If no match is found, the plugin silently skips sync for that book. ### When It Syncs | Event | What happens | |---|---| | **Open a book** | Pulls latest position from server. If the server is ahead, jumps to that position. Also reconciles the book's rating with Tome. | | **Every 50 page turns** | Pushes current position to server (heartbeat). | | **Close the lid (suspend)** | Pushes position, records a reading session, and pushes a rating you set this session. | | **Open the lid (resume)** | Starts a new session, pushes position, and flushes any pending offline sessions. | | **Close a book** | Pushes final position, records a reading session, and pushes a rating you set this session. | | **"Sync now" in menu** | Manual push of current position and flushes pending offline sessions. | ### Web Reader → KOReader Sync Progress saved in Tome's built-in web reader is written to `TomeSyncPosition` on the server. When you next open the same book in KOReader, the plugin detects the server is ahead and jumps to the position you left off at in the browser. This makes the web reader and KOReader fully bidirectional — you can switch between them without losing your place. ### Reading Sessions Every time you close the lid or close a book, the plugin records a session with: - Start and end timestamps - Duration in seconds - Progress at start and end (percentage) - Number of page turns - Device name Sessions shorter than 10 seconds are filtered out. Each session has a unique ID to prevent duplicates. These sessions power the **Stats** page in Tome, showing reading time, streaks, and charts. ### Reading-History Import KOReader keeps its own per-page reading log in `statistics.sqlite3` — often going back years, long before you installed TomeSync. The plugin can import that log into Tome so your **Stats** page reflects reading from before TomeSync existed. - Enable **TomeSync > Auto-sync reading history on launch**, or run it once from **TomeSync > Sync reading history** (also assignable to a gesture). - The first sync pushes your **entire** history; it is **chunked and resumable** — the server tracks a per-device watermark and ignores duplicates, so it survives the device sleeping or Wi-Fi dropping mid-sync. Later syncs send only new reading. - It imports **reading time and pages only** — it never changes your read/unread status. That stays yours to set (only the live progress sync above moves status). - Books are matched to your library by title/series/volume; anything the matcher can't place confidently is left out rather than guessed at, so nothing wrong appears in your stats. Multiple devices each sync their own history without double-counting the same reading. ### Reading Status The server automatically updates the book's reading status based on progress: - **unread** to **reading** when progress > 0% - **reading** to **read** when progress >= 99% ### Series Download The plugin can browse and download entire series directly to your device. **From the home screen (no book open):** 1. Open the wrench menu, find **TomeSync > Browse series** 2. A list shows all series in your Tome library with book counts and authors 3. Tap a series to download all books in it **From inside a book:** - **Download full series** -- downloads every book in the current book's series - **Download rest of series** -- downloads only books after your current volume (based on series_index) **How downloads work:** - Books are organized by book type and series: `///` (e.g. `books/manga/One Piece/`, `books/light_novel/Black Summoner/`). The base directory is KOReader's home folder if set, falling back to `download_dir` then `lastdir`. - The book type (manga, light_novel, book, comic) comes from Tome's metadata - Filenames mirror Tome's OPDS naming: `Vol. N — Title.ext` for volumes in a series, plain `Title.ext` otherwise. This keeps light-novel volumes that share a title from clobbering each other. - Format preference: epub > kepub.epub > cbz > pdf > mobi > azw3 - Books already on the device are skipped (matched by book ID, not filename) - A live progress popup shows "Downloading N of M — Title" as each book is fetched - Downloaded books are automatically registered in the book map, so sync works immediately when you open them - A summary shows downloaded/skipped/failed counts when finished ### Download location & naming By default downloads are filed as described above (`//Vol. N — Title.ext`, standalones under their author). **Settings → Download location & naming** changes this. The setting applies to *every* way books reach the device — the series browser, the in-book series downloads, and the Send-to-KOReader inbox all share one downloader. It is per-device and stored in KOReader's own settings, so your phone can use a different layout than your e-reader, and it survives plugin updates. Three options: - **Default (type and series folders)** — the layout above. This is also what runs when no template is set, on exactly the same code path as before the setting existed. - **Flat in home folder** — every book lands directly in the home folder, named `Series - NN - Title.ext`. Series and volume are dropped for books that have none, so a standalone is just `Title.ext`. For people who keep everything unsorted in one place. - **Custom template** — build the path yourself, Sonarr/Radarr-style. #### Custom templates A template is a **free-form string**: tokens, literal text, and `/` folder separators in any combination. The dialog pre-fills a suggestion (`{book_type}/{series}/{volume:00} - {title}`) purely as a starting point — delete it and write anything. Tokens can repeat, literals can appear anywhere, folders can nest as deep as you like. **Token reference** (the complete set — anything else is rejected): | Token | Renders as | Empty when | |---|---|---| | `{book_type}` | Tome book-type slug (`manga`, `light_novel`, `novel`, ...) | never (falls back to `book`) | | `{series}` | series name | book has no series | | `{volume}` | series index as-is (`3`, `12`, `1.5`) | book has no series index | | `{volume:00}` | zero-padded series index (`03`); pad width = number of zeros, so `{volume:000}` gives `003`. Fractional volumes (`1.5`) are never padded. | book has no series index | | `{title}` | book title | never | | `{author}` | author name | author unknown | **Case modifiers:** wrap a token *name* in `Lower(...)` or `Upper(...)` — `{Lower(series)}`, `{Upper(book_type)}`. No inner braces: `{Lower({series})}` is not valid syntax. **Rules:** - `/` starts a new folder. Everything between slashes becomes one folder (or, for the last part, the filename). - The file extension (`.epub`, `.cbz`, ...) is **appended automatically** — never write it in the template. - Anything that isn't a token is literal text and is kept verbatim. **Examples** (Berserk vol. 3, "The Egg of the King", Kentaro Miura, type `manga`): | Template | Result | |---|---| | `{title}` | `The Egg of the King.epub` | | `{series} - {volume:00} - {title}` | `Berserk - 03 - The Egg of the King.epub` (this is the flat preset) | | `Tome/{series}/{title}` | `Tome/Berserk/The Egg of the King.epub` — literal folder name | | `{author}/{series} {volume:000} {title}` | `Kentaro Miura/Berserk 003 The Egg of the King.epub` | | `{Upper(series)}/vol{volume} - {Lower(title)}` | `BERSERK/vol3 - the egg of the king.epub` | | `{book_type}/{series}/{series} v{volume}` | `manga/Berserk/Berserk v3.epub` — tokens may repeat | **Books without a series (or volume, or author):** empty tokens disappear, *together with orphaned separators around them* — spaces, `-`, `_` and `,` left dangling by a vanished token are tidied up, and a folder segment that renders entirely empty is dropped. So one template serves series books and standalones: | Template | Series book | Standalone | |---|---|---| | `{series} - {volume:00} - {title}` | `Berserk - 03 - The Egg of the King.epub` | `Frankenstein.epub` | | `{book_type}/{series}/{volume:00} - {title}` | `manga/Berserk/03 - The Egg of the King.epub` | `novel/Frankenstein.epub` (series folder dropped) | One caveat: the cleanup removes orphaned *separators*, not orphaned *words*. A literal glued to a token — `Vol. {volume}` — leaves `Vol` behind when the book has no volume (`Vol - Title.epub`). If your library mixes series books and standalones, keep bare separators around tokens that can be empty (`{volume:00} - {title}`), the way both presets do. There is no conditional syntax ("include this text only when the token exists") — if you need that, open an issue. **Validation and safety:** - Templates are validated when you tap **Save**: an unknown token (or a template that renders to nothing) is rejected on the spot with an error, and a valid template shows a preview of the resulting filename. A typo can't silently misfile your downloads. - Every path segment is sanitized for the filesystem, and `..` segments are discarded — a template can never place files outside the download base folder. - A `/` *inside* a metadata value (a title like `Fate/Zero`) becomes `-`; only slashes you write in the template create folders. - If a saved template somehow renders empty for a particular book (e.g. it only uses `{series}` and the book has none), that book falls back to the default layout rather than failing. - Leave the dialog's input empty and save to clear the template and restore the default layout. **Changing the setting later** only affects *future* downloads: books already on the device are remembered by book ID and stay skipped regardless of where they live, and previously downloaded files are not moved. --- ## Offline and Unreachable Server The plugin is designed to work seamlessly when your server is not reachable -- whether you are on a plane, commuting without VPN access, or your server is simply down. ### What happens when you are offline - **No WiFi at all:** Every sync request is skipped instantly. There is zero delay or freezing. You will not notice anything different while reading. - **WiFi connected but server unreachable** (e.g. public WiFi without VPN): Requests time out after 5 seconds. After 3 consecutive failures, the plugin stops trying for the rest of the session. No further delays. - **Reading sessions are saved locally:** If a session cannot be sent to the server (because you are offline when you close the lid), it is saved to disk and retried automatically the next time you open the lid with a working connection. ### Typical commute flow 1. You start reading on the train (no server access) 2. All sync silently skips -- reading is completely uninterrupted 3. You close the lid -- the session is saved locally on your device 4. You get home, open a book -- within the first 50 page turns, the plugin flushes all saved sessions to the server 5. Your Stats page and reading progress are up to date Sessions also flush when you tap "Sync now" in the menu, or when WiFi reconnects and you resume reading. ### Limits - Up to 50 sessions can be saved locally. If you go offline for an extremely long time, the oldest sessions are dropped to prevent unbounded storage use. - Sessions saved locally survive KOReader restarts -- they are stored in KOReader's settings file. - The backoff counter (3 failures before giving up) resets automatically on the next successful request. You can also reset it manually via **Settings → Test connection** in the plugin menu. --- ## Menu The plugin menu is context-aware. It self-registers in the **wrench menu** (after Calibre) under **TomeSync** and shows different options depending on whether a book is open. ### Always visible | Option | Description | |---|---| | **Browse series** | Opens the series browser. Lists all series with book count and author. Tap to download. | | **Sync reading history** | Imports KOReader's per-page reading log into Tome's Stats (time and pages only). First run backfills everything; chunked and resumable. See [Reading-History Import](#reading-history-import). | | **Settings** | Submenu with persistent options and diagnostics (see below). | | **About** | Version info (semver + build). | ### Only when a book is open | Option | Description | |---|---| | **Download full series** | Downloads all books in the current book's series. | | **Download rest of series** | Downloads books after the current volume only. | | **Sync now** | Pushes current position, highlights/notes, and flushes any pending offline sessions. | | **Tracking: on / paused** | Pauses automatic session tracking and syncing for the current KOReader run. Resets to on at the next start — it is not a permanent setting. Manual actions (Sync now, downloads) still work while paused. | | **Pending sessions (N)** | Shows how many sessions are queued for sync. Tap for details. | ### Settings submenu | Option | Description | |---|---| | **Auto-connect WiFi when needed** | Opt-in toggle, off by default. When a TomeSync action needs the server and WiFi is down, KOReader re-establishes the connection first (honouring your KOReader WiFi prompt/auto-enable setting), then runs the action. Helps devices that aggressively sleep WiFi, e.g. PocketBook. Only user-initiated actions reconnect — background tracking never turns the radio on. | | **Download location & naming** | Where downloads (series, inbox) are filed: the default type/series layout, flat in the home folder, or a custom token template. Per-device. See [Download location & naming](#download-location--naming). | | **Test connection** | Verifies the server is reachable. Also resets the backoff counter if the server was previously unreachable. | | **Re-resolve all books** | Wipes the local filename-to-book-ID cache. Use if a book matched incorrectly. | | **Check for updates** | Fetches the latest plugin build from your server and installs it if newer (then prompts to restart). | | **Auto-check for updates on launch** | Opt-in toggle. When on, TomeSync checks for updates shortly after startup and prompts only when one is available. | | **Auto-sync reading history on launch** | Opt-in toggle, off by default. When on, TomeSync pushes new KOReader reading history to Tome shortly after startup (the first run backfills your whole history; chunked and resumable). Reading time and pages only — never your read/unread status. See [Reading-History Import](#reading-history-import). | ### Gestures TomeSync registers bindable gesture actions (KOReader **Settings → Taps and gestures → Gesture manager**, listed under *General* — available in both the reader and the file manager): | Action | Does | |---|---| | **TomeSync: Open menu** | Pops the full context-aware TomeSync menu as a standalone popup. | | **TomeSync: Browse series** | Jumps straight to the series browser/downloader. | | **TomeSync: Sync highlights** | Pushes the current book's highlights and notes to Tome immediately. | | **TomeSync: Sync reading history** | Imports KOReader's per-page reading history into Tome's Stats (time and pages only). | --- ## Highlights & notes sync Highlights and notes you make in KOReader sync to Tome — and **to your other KOReader devices**. They also appear in a **Highlights & Notes** section on the book's detail page (the highlighted text, your note, and the chapter). Sync happens automatically alongside position sync — when you open a book (pulls others' highlights), on suspend, on closing the book, and via **Sync now** — and can be triggered manually with the **TomeSync: Sync highlights** gesture. **How conflicts resolve.** Identity is the highlight's position, so the same passage highlighted on two devices is one highlight. Edits to the same highlight's note resolve last-write-wins (newest edit). Deletes propagate via tombstones — a highlight you remove on one device is removed on the others and won't reappear. Device→device sync needs both devices to have the **Tome-served copy** of the book (identical file → matching positions); a sideloaded different copy won't line up. Rendering highlights *inside the web reader* is a separate, later step — for now the web side shows them as a list on the detail page. --- ## Ratings & reviews sync KOReader has its own **Book status** screen with a 1–5 star rating and a review (long-press a book → *Book status*, or the end-of-book screen). The plugin keeps that in step with the rating and review on Tome's book detail page, **both ways**: - Rate a book **on the web** and the next time you open it in KOReader (for a book that came from Tome), the stars and review are written into the book. - Rate a book **in KOReader** and it flows up to Tome when you close or suspend. Ratings are **per user** and private to you. Reading status (reading / finished) is left untouched — that already syncs separately via reading progress. **How conflicts resolve.** The plugin remembers the last value it synced for each book, so normally only the side that actually changed is sent. If a book's rating changed on **both** the web and the device since the last sync, **Tome wins** — it is the single source of truth. As with highlights, this needs the **Tome-served copy** of the book so the plugin can match it to the right library entry; a sideloaded different copy won't sync its rating. --- ## API Keys The plugin authenticates with an API key (`Authorization: Bearer tk_...`), not your login password. Keys are managed in Tome's settings page: - A key is auto-created when you first download the plugin - You can create additional keys or revoke existing ones - Revoking a key immediately disables any plugin using it --- ## Updating the Plugin Once the plugin is installed, it updates itself — open the **TomeSync** menu and tap **Settings → Check for updates** (or enable **Auto-check for updates on launch**). It downloads the latest build from your server, swaps it in atomically, and asks you to restart. Your server URL, API key, and reading history carry over automatically. The plugin version is shown on the Settings page next to "TomeSync Plugin" (`v (build N)`); the build integer is what drives update comparisons. ### How self-update can't brick the plugin The plugin is split into a frozen **shim** (`main.lua`, never replaced) and a replaceable **implementation** (`main_impl.lua`). On every load the shim runs a rollback state machine backed by `main_impl.lua.bak`: - A **download that fails validation** (too small, doesn't compile, missing sentinels) is rejected before anything is swapped — files untouched. - A **syntax-broken** update fails to load and is rolled back on the **same launch**. - An **init-crashing** update doesn't confirm itself, so the shim rolls it back on the **next launch**. - If even the backup is unusable, the shim loads a valid inert stub ("TomeSync failed to load — reinstall"); KOReader itself is never affected. ### Manual install (first deploy / recovery) The **first** build carrying the shim+impl must be installed by hand once (every update after that is in-app). This is also the fallback if you ever need to force a clean reinstall. #### Via SSH (recommended for Kindle) ```bash # 1. Download plugin ZIP from Tome Settings page, unzip it # 2. Deploy in one command: ssh root@ "rm -rf /mnt/us/koreader/plugins/tomesync.koplugin" && \ scp -r tomesync.koplugin root@:/mnt/us/koreader/plugins/ # 3. Restart KOReader from its menu: Settings > Device > Restart KOReader ``` No cable or USB mode needed. Requires SSH enabled on KOReader (Settings > Network > SSH server). #### Via USB 1. Exit KOReader 2. Connect via USB cable 3. Replace `koreader/plugins/tomesync.koplugin/` with the new version 4. Eject and restart KOReader The downloaded plugin has your server URL and API key baked in, so there is nothing to configure after extracting. --- ## Troubleshooting **Plugin does not appear in the menu:** - Make sure the folder structure is `koreader/plugins/tomesync.koplugin/main.lua` (not double-nested) - Restart KOReader after installing - Check `koreader/crash.log` for errors **"Connection failed" on test:** - Verify your server is running and reachable from the e-reader's network - If you are on a VPN (e.g. Tailscale), make sure it is connected on the e-reader - Re-download the plugin if your server address changed **Wrong book matched:** - Use "Re-resolve all books" in the plugin menu, then re-open the book - The resolve endpoint matches by title and volume number; ambiguous titles may match incorrectly **Progress shows 0% on the web after syncing:** - The plugin only syncs when the book is matched to a Tome book ID - Check the plugin menu: if "Sync now" is greyed out, the book is not matched **Stats page is empty:** - Sessions are recorded when you close the lid or close a book. If you just started using the plugin, read for a while and close the lid to generate your first session. - If you were previously using an older version of the plugin that only recorded sessions on book close, re-download the latest version. The new version records a session every time you close the lid. **KOReader froze or became unresponsive:** - Older versions of the plugin could freeze KOReader for up to 60 seconds per request when the server was unreachable. Re-download the latest version, which has a 5-second timeout and automatic backoff. --- ## Files The plugin consists of three files (plus a backup created on first update): | File | Purpose | |---|---| | `_meta.lua` | Plugin metadata (name, description) | | `main.lua` | Frozen stable shim — loads the implementation and runs the anti-brick rollback state machine. No config; never replaced by self-update. | | `main_impl.lua` | All plugin logic, HTTP client, and config (server URL, API key baked in at download time). The only file self-update replaces. | | `main_impl.lua.bak` | Last confirmed-good implementation, written automatically before each update so the shim can roll back. |