koreader_D00901 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seth Barrett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: koreader_D00901
3
+ Version: 0.1.0
4
+ Summary: Install KOReader on a Kindle Keyboard (K3) from Linux.
5
+ Keywords: kindle,koreader,kindle-keyboard,k3,jailbreak,e-reader
6
+ Author: Seth Barrett
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: System :: Installation/Setup
17
+ Requires-Dist: textual>=8.2,<9
18
+ Requires-Python: >=3.14
19
+ Project-URL: Homepage, https://github.com/sethbarrett50/koreader_D00901
20
+ Project-URL: Issues, https://github.com/sethbarrett50/koreader_D00901/issues
21
+ Description-Content-Type: text/markdown
22
+
23
+ # KOReader installer for the Kindle Keyboard (K3)
24
+
25
+ Use this at your own risk: it modifies your Kindle's firmware and I am not responsible for a unit that stops working.
26
+
27
+ ## What this does
28
+
29
+ `koreader-d00901` (the `koreader_D00901` Python package) downloads pinned, checksum-verified packages and stages them on a Kindle Keyboard (K3) that is mounted as a USB drive. It chains:
30
+
31
+ 1. The jailbreak, which lets the Kindle install custom update packages.
32
+ 2. MKK (the developer certificates the Kindle needs to run custom Kindlets such as KUAL).
33
+ MKK's 2014 certificates expired in April 2025, so the installer then applies NiLuJe's 2025 developer keystore update, which adds the certificates current KUAL is signed with.
34
+ 3. KUAL (the Kindle Unified Application Launcher), copied into `documents/`.
35
+ 4. KOReader, merged into the Kindle's root.
36
+ 5. Optionally, USBNetwork (`--usbnet`), for ssh access over USB.
37
+
38
+ ## Before you start
39
+
40
+ - Charge the Kindle fully.
41
+ - Deregister it: Settings > Registration > Deregister.
42
+ - Turn wireless off.
43
+ - Find your firmware version at the bottom of the Settings page (for example 3.4.2).
44
+ - Find your serial prefix: the first four characters of the serial number (B006, B008 or B00A), listed under Device Info or on the back of the unit.
45
+
46
+ ## Which files your Kindle needs
47
+
48
+ The installer picks the right files from your serial prefix and firmware range.
49
+
50
+ | Serial prefix | Variant |
51
+ | ------------- | ------- |
52
+ | B006 | k3g |
53
+ | B008 | k3w |
54
+ | B00A | k3gb |
55
+
56
+ Firmware 3.0-3.2 uses the jailbreak file marked `-3.0-to-3.2`. Firmware 3.3-3.4 uses the plain jailbreak file. The installer asks for the range as `3.0-3.2` or `3.3-3.4`.
57
+
58
+ ## Running the installer
59
+
60
+ Install from PyPI, then connect the Kindle in USB drive mode and start the full-screen installer:
61
+
62
+ ```
63
+ uv tool install koreader_D00901 # or: pipx install koreader_D00901
64
+ koreader-d00901-tui
65
+ ```
66
+
67
+ To run it once without installing anything, use `uvx --from koreader_D00901 koreader-d00901-tui`. From a clone of this repo, use `uv run koreader-d00901-tui`.
68
+
69
+ Pick the firmware range and serial prefix, press **Install**, and follow the on-screen steps. Between updates it shows what to do on the Kindle, has an **Eject Kindle** button, and waits for **Continue**. **Dry run** shows the plan without changing anything.
70
+
71
+ The same install also runs as a plain terminal script. Do a dry run first: it prints the plan and changes nothing.
72
+
73
+ ```
74
+ uv run koreader-d00901 --dry-run
75
+ ```
76
+
77
+ Then run it for real:
78
+
79
+ ```
80
+ uv run koreader-d00901
81
+ ```
82
+
83
+ Flags (both commands take the same ones, except that `--dry-run` is CLI-only):
84
+
85
+ - `--dry-run`: print the plan only.
86
+ - `--mount PATH`: the Kindle's USB mount point. Without it the installer looks for a volume labelled Kindle.
87
+ - `--fw RANGE`: firmware range, `3.0-3.2` or `3.3-3.4`. Asked interactively if omitted.
88
+ - `--prefix PREFIX`: serial prefix, `B006`, `B008` or `B00A`. Asked interactively if omitted.
89
+ - `--usbnet`: also install USBNetwork.
90
+ - `--wipe-books`: delete the existing books in `documents/` before installing, which is handy for second-hand units. It lists everything it will delete and asks you to type `wipe` (the TUI shows a confirmation dialog). It keeps `KUAL-KDK-1.0.azw2` and the `dictionaries` folder, and never touches anything outside `documents/`. With `--dry-run` it only lists what it would delete.
91
+ - `--cache DIR`: where downloads are kept (default `~/.cache/k3-koreader`).
92
+ - `--log-file FILE`: where the debug log goes (default `CACHE/install.log`).
93
+ - `-v`, `--verbose`: also print debug logging on the terminal (CLI only).
94
+ - `--version`: print the version.
95
+
96
+ Every run writes a debug log. If an install fails, check `~/.cache/k3-koreader/install.log` first.
97
+
98
+ Each download is checked against the sha256 in `packages.json` and rejected if it does not match.
99
+
100
+ ## On the Kindle
101
+
102
+ For each update the installer stages one `.bin` file and pauses. Then:
103
+
104
+ 1. Eject and unplug the Kindle (safely eject it first; the update cannot run in USB drive mode).
105
+ 2. Press [HOME] > [MENU] > Settings > [MENU] > Update Your Kindle.
106
+ 3. Wait for the reboot, reconnect in USB drive mode and press Enter in the installer.
107
+
108
+ The order is jailbreak, then MKK, then the 2025 keystore (then USBNetwork if requested). After the last step, eject the Kindle, open KUAL from the library and choose KOReader.
109
+
110
+ ## If something goes wrong
111
+
112
+ - Update Your Kindle is greyed out: the wrong file was staged. Check your serial prefix and firmware range and run the installer again.
113
+ - Never force an update by rebooting the Kindle by hand.
114
+ - If the Kindle hangs, hold the power button for about 15 seconds for a hard reboot.
115
+ - Deregistering or an over-the-air (OTA) update can remove the developer certificates. Reinstall MKK when that happens.
116
+ - KUAL says the title is "not signed by a registered developer": the 2025 keystore update did not apply. Copy `Update-mkk-20250419-<variant>-<prefix>_keystore-install.bin` from `DevCerts-20250419-KeyStore.zip` to the Kindle root and run Update Your Kindle again.
117
+
118
+ ## USBNetwork (optional)
119
+
120
+ After installing with `--usbnet`, enable it from the Kindle's search bar:
121
+
122
+ 1. Type `;debugOn` and press Enter.
123
+ 2. Type `~usbNetwork` and press Enter.
124
+ 3. Type `;debugOff` and press Enter.
125
+
126
+ Then connect over USB. On Debian the Kindle appears as a USB network interface, often `usb0` (check with `ip link`). Assign the computer side its address:
127
+
128
+ ```
129
+ sudo ip addr add 192.168.2.1/24 dev usb0
130
+ sudo ip link set usb0 up
131
+ ```
132
+
133
+ (The older equivalent is `sudo ifconfig usb0 192.168.2.1`.) Then use `ssh root@192.168.2.2`. Login is passwordless only over USB with Wi-Fi mode off.
134
+
135
+ Before ejecting, bring the interface down with `sudo ip link set usb0 down`, and enter `~usbNetwork` again on the Kindle to switch back to USB drive mode.
136
+
137
+ ## Pinned packages
138
+
139
+ | Package | File | sha256 (first 12) |
140
+ | ---------- | ---------------------------------------- | ----------------- |
141
+ | jailbreak | kindle-jailbreak-0.13.N-r18833.tar.xz | 8f5c8a78e3c4 |
142
+ | mkk | kindle-mkk-20141129-r18833.tar.xz | 3d7289f1ddcf |
143
+ | keystore | DevCerts-20250419-KeyStore.zip | 48426add32e3 |
144
+ | kual | KUAL-v2.7.37-gfcb45b5-20250419.tar.xz | 40c4b6871221 |
145
+ | usbnet | kindle-usbnetwork-0.57.N-k3.zip | 7d210bc41619 |
146
+ | koreader | koreader-kindle-legacy-v2026.07.1.zip | 0ed8d5eec422 |
147
+
148
+ ## Credits
149
+
150
+ - Jailbreak: https://www.mobileread.com/forums/showthread.php?t=225030
151
+ - 2025 developer keystore (NiLuJe, 2025-04-19): https://www.mobileread.com/forums/attachment.php?attachmentid=215127&d=1745098511
152
+ - KUAL: https://www.mobileread.com/forums/showthread.php?t=203326
153
+ - USBNetwork: https://www.mobileread.com/forums/showthread.php?t=88004
154
+ - KOReader: https://github.com/koreader/koreader
155
+
156
+ ## Development
157
+
158
+ ```
159
+ make sync # uv sync --dev (ruff, ty, pytest)
160
+ make lint # ruff format + ruff check --fix
161
+ make typecheck # ty check
162
+ make test # pytest
163
+ make ci # everything CI runs, without fixes
164
+ make build # sdist + wheel into dist/
165
+ make smoke # install the wheel in a clean env and run both commands
166
+ make version-check # package changed since the last tag? then the version must be bumped
167
+ uv run python -m koreader_d00901.pins # re-download every pin and check it (about 100 MB)
168
+ ```
169
+
170
+ CI (`.github/workflows/ci.yml`) runs `make ci`, `make build` and `make smoke` on Python 3.14 for every push and PR. A weekly pin check (`.github/workflows/pins.yml`, also runnable from the Actions tab) re-downloads every package in `packages.json`, verifies its sha256, and confirms that it still holds every file the installer stages for each serial prefix and firmware range. If upstream drifts, it opens an issue (or comments on the open one).
171
+
172
+ ### Releasing
173
+
174
+ 1. Bump `version` in `pyproject.toml` (`uv version --bump patch`, `minor` or `major`), then merge to `main`. CI's `make version-check` fails any PR that changes `src/`, the dependencies or `uv.lock` since the last `vX.Y.Z` tag without raising the version, so every release gets a new version on PyPI.
175
+ 2. Tag the merge commit `vX.Y.Z` (matching that version) and push the tag: `git tag v0.1.0 && git push origin v0.1.0`.
176
+ 3. `.github/workflows/release.yml` checks the tag against the version, runs `make ci`, builds and smoke-tests the wheel, and publishes it to PyPI through trusted publishing (no API token is stored anywhere).
177
+
178
+ One-time setup on PyPI, before the first release: at https://pypi.org/manage/account/publishing/ add a pending trusted publisher with project `koreader_D00901`, owner `sethbarrett50`, repository `koreader_D00901`, workflow `release.yml` and environment `pypi`. In the GitHub repo settings, create an environment named `pypi` (adding yourself as a required reviewer gives you a manual approval step before each upload).
179
+
180
+ The code lives in `src/koreader_d00901/`:
181
+
182
+ - `plan.py`: which files a given serial prefix and firmware range need.
183
+ - `packages.py`: downloading, verifying and unpacking the packages pinned in `packages.json`.
184
+ - `device.py`: everything that touches the Kindle's USB volume.
185
+ - `cli.py` / `tui.py`: the two front ends.
186
+ - `errors.py`: `InstallerError` and its subclasses, which are reported to the user as one line.
187
+ - `logs.py`: logging setup (a debug log file on every run).
188
+ - `pins.py`: the upstream pin check used by the weekly workflow.
@@ -0,0 +1,166 @@
1
+ # KOReader installer for the Kindle Keyboard (K3)
2
+
3
+ Use this at your own risk: it modifies your Kindle's firmware and I am not responsible for a unit that stops working.
4
+
5
+ ## What this does
6
+
7
+ `koreader-d00901` (the `koreader_D00901` Python package) downloads pinned, checksum-verified packages and stages them on a Kindle Keyboard (K3) that is mounted as a USB drive. It chains:
8
+
9
+ 1. The jailbreak, which lets the Kindle install custom update packages.
10
+ 2. MKK (the developer certificates the Kindle needs to run custom Kindlets such as KUAL).
11
+ MKK's 2014 certificates expired in April 2025, so the installer then applies NiLuJe's 2025 developer keystore update, which adds the certificates current KUAL is signed with.
12
+ 3. KUAL (the Kindle Unified Application Launcher), copied into `documents/`.
13
+ 4. KOReader, merged into the Kindle's root.
14
+ 5. Optionally, USBNetwork (`--usbnet`), for ssh access over USB.
15
+
16
+ ## Before you start
17
+
18
+ - Charge the Kindle fully.
19
+ - Deregister it: Settings > Registration > Deregister.
20
+ - Turn wireless off.
21
+ - Find your firmware version at the bottom of the Settings page (for example 3.4.2).
22
+ - Find your serial prefix: the first four characters of the serial number (B006, B008 or B00A), listed under Device Info or on the back of the unit.
23
+
24
+ ## Which files your Kindle needs
25
+
26
+ The installer picks the right files from your serial prefix and firmware range.
27
+
28
+ | Serial prefix | Variant |
29
+ | ------------- | ------- |
30
+ | B006 | k3g |
31
+ | B008 | k3w |
32
+ | B00A | k3gb |
33
+
34
+ Firmware 3.0-3.2 uses the jailbreak file marked `-3.0-to-3.2`. Firmware 3.3-3.4 uses the plain jailbreak file. The installer asks for the range as `3.0-3.2` or `3.3-3.4`.
35
+
36
+ ## Running the installer
37
+
38
+ Install from PyPI, then connect the Kindle in USB drive mode and start the full-screen installer:
39
+
40
+ ```
41
+ uv tool install koreader_D00901 # or: pipx install koreader_D00901
42
+ koreader-d00901-tui
43
+ ```
44
+
45
+ To run it once without installing anything, use `uvx --from koreader_D00901 koreader-d00901-tui`. From a clone of this repo, use `uv run koreader-d00901-tui`.
46
+
47
+ Pick the firmware range and serial prefix, press **Install**, and follow the on-screen steps. Between updates it shows what to do on the Kindle, has an **Eject Kindle** button, and waits for **Continue**. **Dry run** shows the plan without changing anything.
48
+
49
+ The same install also runs as a plain terminal script. Do a dry run first: it prints the plan and changes nothing.
50
+
51
+ ```
52
+ uv run koreader-d00901 --dry-run
53
+ ```
54
+
55
+ Then run it for real:
56
+
57
+ ```
58
+ uv run koreader-d00901
59
+ ```
60
+
61
+ Flags (both commands take the same ones, except that `--dry-run` is CLI-only):
62
+
63
+ - `--dry-run`: print the plan only.
64
+ - `--mount PATH`: the Kindle's USB mount point. Without it the installer looks for a volume labelled Kindle.
65
+ - `--fw RANGE`: firmware range, `3.0-3.2` or `3.3-3.4`. Asked interactively if omitted.
66
+ - `--prefix PREFIX`: serial prefix, `B006`, `B008` or `B00A`. Asked interactively if omitted.
67
+ - `--usbnet`: also install USBNetwork.
68
+ - `--wipe-books`: delete the existing books in `documents/` before installing, which is handy for second-hand units. It lists everything it will delete and asks you to type `wipe` (the TUI shows a confirmation dialog). It keeps `KUAL-KDK-1.0.azw2` and the `dictionaries` folder, and never touches anything outside `documents/`. With `--dry-run` it only lists what it would delete.
69
+ - `--cache DIR`: where downloads are kept (default `~/.cache/k3-koreader`).
70
+ - `--log-file FILE`: where the debug log goes (default `CACHE/install.log`).
71
+ - `-v`, `--verbose`: also print debug logging on the terminal (CLI only).
72
+ - `--version`: print the version.
73
+
74
+ Every run writes a debug log. If an install fails, check `~/.cache/k3-koreader/install.log` first.
75
+
76
+ Each download is checked against the sha256 in `packages.json` and rejected if it does not match.
77
+
78
+ ## On the Kindle
79
+
80
+ For each update the installer stages one `.bin` file and pauses. Then:
81
+
82
+ 1. Eject and unplug the Kindle (safely eject it first; the update cannot run in USB drive mode).
83
+ 2. Press [HOME] > [MENU] > Settings > [MENU] > Update Your Kindle.
84
+ 3. Wait for the reboot, reconnect in USB drive mode and press Enter in the installer.
85
+
86
+ The order is jailbreak, then MKK, then the 2025 keystore (then USBNetwork if requested). After the last step, eject the Kindle, open KUAL from the library and choose KOReader.
87
+
88
+ ## If something goes wrong
89
+
90
+ - Update Your Kindle is greyed out: the wrong file was staged. Check your serial prefix and firmware range and run the installer again.
91
+ - Never force an update by rebooting the Kindle by hand.
92
+ - If the Kindle hangs, hold the power button for about 15 seconds for a hard reboot.
93
+ - Deregistering or an over-the-air (OTA) update can remove the developer certificates. Reinstall MKK when that happens.
94
+ - KUAL says the title is "not signed by a registered developer": the 2025 keystore update did not apply. Copy `Update-mkk-20250419-<variant>-<prefix>_keystore-install.bin` from `DevCerts-20250419-KeyStore.zip` to the Kindle root and run Update Your Kindle again.
95
+
96
+ ## USBNetwork (optional)
97
+
98
+ After installing with `--usbnet`, enable it from the Kindle's search bar:
99
+
100
+ 1. Type `;debugOn` and press Enter.
101
+ 2. Type `~usbNetwork` and press Enter.
102
+ 3. Type `;debugOff` and press Enter.
103
+
104
+ Then connect over USB. On Debian the Kindle appears as a USB network interface, often `usb0` (check with `ip link`). Assign the computer side its address:
105
+
106
+ ```
107
+ sudo ip addr add 192.168.2.1/24 dev usb0
108
+ sudo ip link set usb0 up
109
+ ```
110
+
111
+ (The older equivalent is `sudo ifconfig usb0 192.168.2.1`.) Then use `ssh root@192.168.2.2`. Login is passwordless only over USB with Wi-Fi mode off.
112
+
113
+ Before ejecting, bring the interface down with `sudo ip link set usb0 down`, and enter `~usbNetwork` again on the Kindle to switch back to USB drive mode.
114
+
115
+ ## Pinned packages
116
+
117
+ | Package | File | sha256 (first 12) |
118
+ | ---------- | ---------------------------------------- | ----------------- |
119
+ | jailbreak | kindle-jailbreak-0.13.N-r18833.tar.xz | 8f5c8a78e3c4 |
120
+ | mkk | kindle-mkk-20141129-r18833.tar.xz | 3d7289f1ddcf |
121
+ | keystore | DevCerts-20250419-KeyStore.zip | 48426add32e3 |
122
+ | kual | KUAL-v2.7.37-gfcb45b5-20250419.tar.xz | 40c4b6871221 |
123
+ | usbnet | kindle-usbnetwork-0.57.N-k3.zip | 7d210bc41619 |
124
+ | koreader | koreader-kindle-legacy-v2026.07.1.zip | 0ed8d5eec422 |
125
+
126
+ ## Credits
127
+
128
+ - Jailbreak: https://www.mobileread.com/forums/showthread.php?t=225030
129
+ - 2025 developer keystore (NiLuJe, 2025-04-19): https://www.mobileread.com/forums/attachment.php?attachmentid=215127&d=1745098511
130
+ - KUAL: https://www.mobileread.com/forums/showthread.php?t=203326
131
+ - USBNetwork: https://www.mobileread.com/forums/showthread.php?t=88004
132
+ - KOReader: https://github.com/koreader/koreader
133
+
134
+ ## Development
135
+
136
+ ```
137
+ make sync # uv sync --dev (ruff, ty, pytest)
138
+ make lint # ruff format + ruff check --fix
139
+ make typecheck # ty check
140
+ make test # pytest
141
+ make ci # everything CI runs, without fixes
142
+ make build # sdist + wheel into dist/
143
+ make smoke # install the wheel in a clean env and run both commands
144
+ make version-check # package changed since the last tag? then the version must be bumped
145
+ uv run python -m koreader_d00901.pins # re-download every pin and check it (about 100 MB)
146
+ ```
147
+
148
+ CI (`.github/workflows/ci.yml`) runs `make ci`, `make build` and `make smoke` on Python 3.14 for every push and PR. A weekly pin check (`.github/workflows/pins.yml`, also runnable from the Actions tab) re-downloads every package in `packages.json`, verifies its sha256, and confirms that it still holds every file the installer stages for each serial prefix and firmware range. If upstream drifts, it opens an issue (or comments on the open one).
149
+
150
+ ### Releasing
151
+
152
+ 1. Bump `version` in `pyproject.toml` (`uv version --bump patch`, `minor` or `major`), then merge to `main`. CI's `make version-check` fails any PR that changes `src/`, the dependencies or `uv.lock` since the last `vX.Y.Z` tag without raising the version, so every release gets a new version on PyPI.
153
+ 2. Tag the merge commit `vX.Y.Z` (matching that version) and push the tag: `git tag v0.1.0 && git push origin v0.1.0`.
154
+ 3. `.github/workflows/release.yml` checks the tag against the version, runs `make ci`, builds and smoke-tests the wheel, and publishes it to PyPI through trusted publishing (no API token is stored anywhere).
155
+
156
+ One-time setup on PyPI, before the first release: at https://pypi.org/manage/account/publishing/ add a pending trusted publisher with project `koreader_D00901`, owner `sethbarrett50`, repository `koreader_D00901`, workflow `release.yml` and environment `pypi`. In the GitHub repo settings, create an environment named `pypi` (adding yourself as a required reviewer gives you a manual approval step before each upload).
157
+
158
+ The code lives in `src/koreader_d00901/`:
159
+
160
+ - `plan.py`: which files a given serial prefix and firmware range need.
161
+ - `packages.py`: downloading, verifying and unpacking the packages pinned in `packages.json`.
162
+ - `device.py`: everything that touches the Kindle's USB volume.
163
+ - `cli.py` / `tui.py`: the two front ends.
164
+ - `errors.py`: `InstallerError` and its subclasses, which are reported to the user as one line.
165
+ - `logs.py`: logging setup (a debug log file on every run).
166
+ - `pins.py`: the upstream pin check used by the weekly workflow.
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "koreader_D00901"
3
+ version = "0.1.0"
4
+ description = "Install KOReader on a Kindle Keyboard (K3) from Linux."
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "kindle",
11
+ "koreader",
12
+ "kindle-keyboard",
13
+ "k3",
14
+ "jailbreak",
15
+ "e-reader",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: End Users/Desktop",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Topic :: System :: Installation/Setup",
26
+ ]
27
+ dependencies = ["textual>=8.2,<9"]
28
+
29
+ [[project.authors]]
30
+ name = "Seth Barrett"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/sethbarrett50/koreader_D00901"
34
+ Issues = "https://github.com/sethbarrett50/koreader_D00901/issues"
35
+
36
+ [project.scripts]
37
+ koreader-d00901 = "koreader_d00901.cli:main"
38
+ koreader-d00901-tui = "koreader_d00901.tui:main"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.9,<0.12"]
42
+ build-backend = "uv_build"
43
+
44
+ [tool.uv.build-backend]
45
+ module-name = "koreader_d00901"
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ty.environment]
51
+ python-version = "3.14"
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "ruff>=0.15.15,<1.0.0",
56
+ "ty>=0.0.97,<0.1.0",
57
+ "pytest>=9.0.3,<10.0.0",
58
+ ]
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "koreader_D00901"
3
+ version = "0.1.0"
4
+ description = "Install KOReader on a Kindle Keyboard (K3) from Linux."
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ authors = [{ name = "Seth Barrett" }]
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ keywords = ["kindle", "koreader", "kindle-keyboard", "k3", "jailbreak", "e-reader"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Console",
14
+ "Intended Audience :: End Users/Desktop",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Topic :: System :: Installation/Setup",
20
+ ]
21
+ dependencies = [
22
+ "textual>=8.2,<9",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/sethbarrett50/koreader_D00901"
27
+ Issues = "https://github.com/sethbarrett50/koreader_D00901/issues"
28
+
29
+ [project.scripts]
30
+ koreader-d00901 = "koreader_d00901.cli:main"
31
+ koreader-d00901-tui = "koreader_d00901.tui:main"
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.9,<0.12"]
35
+ build-backend = "uv_build"
36
+
37
+ [tool.uv.build-backend]
38
+ module-name = "koreader_d00901"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "ruff>=0.15.15,<1.0.0",
43
+ "ty>=0.0.97,<0.1.0",
44
+ "pytest>=9.0.3,<10.0.0",
45
+ ]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ty.environment]
51
+ python-version = "3.14"
@@ -0,0 +1,8 @@
1
+ """Install KOReader on a Kindle Keyboard (K3) from Linux."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version('koreader_D00901')
7
+ except PackageNotFoundError: # running from a source tree that was never installed
8
+ __version__ = '0.0.0'
@@ -0,0 +1,3 @@
1
+ from koreader_d00901.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,168 @@
1
+ """Command-line installer: prompts on the terminal and waits for Enter between device updates."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from koreader_d00901 import __version__, device, packages, plan
11
+ from koreader_d00901.errors import ConfigError, InstallerError
12
+ from koreader_d00901.logs import DEFAULT_CACHE, setup_logging
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Callable
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+ DEVICE_STEPS = (
20
+ ' First safely eject (unmount) the Kindle and unplug it from the computer;\n'
21
+ ' the update cannot run while it is in USB drive mode.\n'
22
+ ' Then on the Kindle: [HOME] > [MENU] > Settings > [MENU] > Update Your Kindle.\n'
23
+ ' Never reboot the Kindle by hand to force the update.\n'
24
+ " If 'Update Your Kindle' is greyed out, the wrong file was staged."
25
+ )
26
+ DONE_MESSAGE = 'Done. Eject the Kindle, then on the device open the KUAL book in your library and choose KOReader.'
27
+
28
+
29
+ def add_common_args(parser: argparse.ArgumentParser) -> None:
30
+ parser.add_argument('--mount', type=Path, help='Kindle USB mount point (auto-detected if omitted)')
31
+ parser.add_argument('--fw', help='firmware range: ' + ', '.join(plan.FIRMWARE_RANGES))
32
+ parser.add_argument('--prefix', help='serial prefix: ' + ', '.join(plan.VARIANTS))
33
+ parser.add_argument('--usbnet', action='store_true', help='also install USBNetwork')
34
+ parser.add_argument(
35
+ '--wipe-books', action='store_true', help='delete the existing books in documents/ first (asks to confirm)'
36
+ )
37
+ parser.add_argument('--cache', type=Path, default=DEFAULT_CACHE, help='download cache directory')
38
+ parser.add_argument('--log-file', type=Path, help='debug log file (default CACHE/install.log)')
39
+ parser.add_argument('-v', '--verbose', action='store_true', help='show debug logging on the console')
40
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
41
+
42
+
43
+ def _ask_valid(prompt: str, value: str | None, check: Callable[[str], str], flag: str) -> str:
44
+ """Validate a flag value (fatal when bad) or prompt until one validates."""
45
+ if value is not None:
46
+ try:
47
+ return check(value)
48
+ except ConfigError as exc:
49
+ raise ConfigError(f'{flag}: {exc}') from None
50
+ while True:
51
+ try:
52
+ return check(input(prompt))
53
+ except ConfigError as exc:
54
+ print(f' {exc}')
55
+
56
+
57
+ def _copy_with_progress(pairs: list[tuple[Path, Path]], label: str) -> None:
58
+ """Copy (src, dst) file pairs, printing a running count so slow USB writes don't look hung."""
59
+ every = max(1, len(pairs) // 100)
60
+
61
+ def show(i: int, total: int) -> None:
62
+ if i % every == 0 or i == total:
63
+ print(f'\r {label}: {i}/{total} files', end='', flush=True)
64
+
65
+ device.copy_files(pairs, show)
66
+ print()
67
+
68
+
69
+ def _update_step(mount: Path, bin_path: Path, label: str) -> None:
70
+ device.stage_update(mount, bin_path)
71
+ print(f'\nStaged {bin_path.name} ({label}).')
72
+ print(DEVICE_STEPS)
73
+ input('Press Enter once the Kindle has rebooted and remounted... ')
74
+ while not mount.is_dir():
75
+ log.debug('waiting for %s to come back', mount)
76
+ input(f'Kindle not mounted at {mount} yet; reconnect it in USB drive mode and press Enter... ')
77
+ if device.update_pending(mount, bin_path):
78
+ log.warning('%s is still at the Kindle root; the update may not have applied.', bin_path.name)
79
+
80
+
81
+ def describe(entries: list[Path], limit: int = 20) -> list[str]:
82
+ """Display lines for entries of documents/, capped at limit."""
83
+ lines = [f' {p.name}{"/" if p.is_dir() else ""}' for p in entries[:limit]]
84
+ if len(entries) > limit:
85
+ lines.append(f' ...and {len(entries) - limit} more')
86
+ return lines
87
+
88
+
89
+ def _wipe_step(mount: Path, dry_run: bool) -> None:
90
+ entries = device.list_books(mount)
91
+ if not entries:
92
+ print('No books to delete in documents/.')
93
+ return
94
+ verb = 'would delete' if dry_run else 'will delete'
95
+ print(f'--wipe-books {verb} {len(entries)} entries in documents/ (keeping KUAL and dictionaries):')
96
+ print('\n'.join(describe(entries)))
97
+ if dry_run:
98
+ return
99
+ answer = input(f"Type 'wipe' to delete these {len(entries)} entries (anything else skips this step): ")
100
+ if answer.strip().lower() != 'wipe':
101
+ print('Skipped; no books were deleted.')
102
+ log.info('wipe declined')
103
+ return
104
+ removed = device.wipe_books(mount, entries)
105
+ print(f'Deleted {removed} entries.')
106
+
107
+
108
+ def run(args: argparse.Namespace) -> int:
109
+ fw = _ask_valid(f'Firmware range ({"/".join(plan.FIRMWARE_RANGES)}): ', args.fw, plan.check_firmware, '--fw')
110
+ prefix = _ask_valid(f'Serial prefix ({"/".join(plan.VARIANTS)}): ', args.prefix, plan.check_prefix, '--prefix')
111
+ files = plan.select_files(fw, prefix, args.usbnet)
112
+ mount = device.require_mount(args.mount)
113
+ steps = plan.update_steps(files)
114
+ log.debug('plan: fw=%s prefix=%s usbnet=%s mount=%s files=%s', fw, prefix, args.usbnet, mount, files)
115
+
116
+ if args.dry_run:
117
+ print(f'Dry run: mount={mount} firmware={fw} prefix={prefix}')
118
+ if args.wipe_books:
119
+ _wipe_step(mount, dry_run=True)
120
+ for key, label in steps:
121
+ print(f' would stage {files[key]} ({label})')
122
+ print(f' would copy {plan.KUAL_FILE} into documents/')
123
+ print(' would merge koreader/, extensions/, launchpad/ into the mount root')
124
+ return 0
125
+
126
+ if args.wipe_books:
127
+ _wipe_step(mount, dry_run=False)
128
+
129
+ print('Getting packages (cached after the first run):')
130
+ roots = packages.fetch_packages(plan.package_keys(files), args.cache, lambda name: print(f' {name}', flush=True))
131
+ bins = [(packages.member_path(roots[key], files[key]), label) for key, label in steps]
132
+ kual = packages.member_path(roots['kual'], files['kual'])
133
+ for bin_path, label in bins:
134
+ _update_step(mount, bin_path, label)
135
+
136
+ print(f'\nCopying {kual.name} to documents/')
137
+ device.install_kual(mount, kual)
138
+
139
+ pairs = device.koreader_pairs(roots['koreader'], mount)
140
+ size_mb = sum(src.stat().st_size for src, _ in pairs) / 1e6
141
+ print(
142
+ f'Copying KOReader ({len(pairs)} files, {size_mb:.0f} MB). '
143
+ 'This takes several minutes on a K3; leave it plugged in.'
144
+ )
145
+ _copy_with_progress(pairs, 'KOReader')
146
+ print('Flushing writes to the Kindle...', flush=True)
147
+ device.flush()
148
+ print(f'\n{DONE_MESSAGE}')
149
+ return 0
150
+
151
+
152
+ def main(argv: list[str] | None = None) -> int:
153
+ parser = argparse.ArgumentParser(prog='koreader-d00901', description='Install KOReader on a Kindle K3.')
154
+ add_common_args(parser)
155
+ parser.add_argument('--dry-run', action='store_true', help='print the plan only')
156
+ args = parser.parse_args(argv)
157
+ log_file = args.log_file or args.cache / 'install.log'
158
+ setup_logging(log_file, verbose=args.verbose)
159
+ log.debug('koreader-d00901 %s, args=%s', __version__, vars(args))
160
+ try:
161
+ return run(args)
162
+ except InstallerError as exc:
163
+ log.debug('install failed', exc_info=True)
164
+ print(f'error: {exc}\n(details in {log_file})', file=sys.stderr)
165
+ return 1
166
+ except KeyboardInterrupt:
167
+ print('\nInterrupted.')
168
+ return 130