kobobridge 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.
- kobobridge-0.1.0/.gitignore +7 -0
- kobobridge-0.1.0/CHANGELOG.md +11 -0
- kobobridge-0.1.0/Dockerfile +17 -0
- kobobridge-0.1.0/LICENSE +21 -0
- kobobridge-0.1.0/PKG-INFO +177 -0
- kobobridge-0.1.0/README.md +130 -0
- kobobridge-0.1.0/pyproject.toml +52 -0
- kobobridge-0.1.0/src/kobobridge/__init__.py +12 -0
- kobobridge-0.1.0/src/kobobridge/__main__.py +133 -0
- kobobridge-0.1.0/src/kobobridge/abs.py +207 -0
- kobobridge-0.1.0/src/kobobridge/app.py +92 -0
- kobobridge-0.1.0/src/kobobridge/config.py +63 -0
- kobobridge-0.1.0/src/kobobridge/kobo.py +377 -0
- kobobridge-0.1.0/src/kobobridge/resources.py +123 -0
- kobobridge-0.1.0/src/kobobridge/state.py +80 -0
- kobobridge-0.1.0/src/kobobridge/synctoken.py +70 -0
- kobobridge-0.1.0/tests/conftest.py +153 -0
- kobobridge-0.1.0/tests/test_abs.py +111 -0
- kobobridge-0.1.0/tests/test_config.py +70 -0
- kobobridge-0.1.0/tests/test_kobo.py +293 -0
- kobobridge-0.1.0/tests/test_synctoken.py +35 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Serves the eReader sync protocol in front of an Audiobookshelf library, read only.
|
|
8
|
+
- Sends epub and kepub files, with covers, authors, series and descriptions.
|
|
9
|
+
- Cuts large libraries into batches so a first sync completes.
|
|
10
|
+
- Keeps reading progress pushed by the reader in a local file, never in Audiobookshelf.
|
|
11
|
+
- No telemetry, and no call to any server other than your own Audiobookshelf.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
5
|
+
COPY src ./src
|
|
6
|
+
RUN pip install --no-cache-dir .
|
|
7
|
+
|
|
8
|
+
ENV KOBOBRIDGE_PORT=8484
|
|
9
|
+
ENV XDG_DATA_HOME=/data
|
|
10
|
+
VOLUME ["/data"]
|
|
11
|
+
EXPOSE 8484
|
|
12
|
+
|
|
13
|
+
RUN useradd --create-home --uid 10001 bridge && mkdir -p /data && chown bridge /data
|
|
14
|
+
USER bridge
|
|
15
|
+
|
|
16
|
+
ENTRYPOINT ["kobobridge"]
|
|
17
|
+
CMD ["run", "--host", "0.0.0.0", "--port", "8484"]
|
kobobridge-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Younes Z.
|
|
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,177 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kobobridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Serve an Audiobookshelf ebook library to a Kobo eReader over the native sync protocol.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Rezarys/kobobridge
|
|
6
|
+
Project-URL: Source, https://github.com/Rezarys/kobobridge
|
|
7
|
+
Project-URL: Issues, https://github.com/Rezarys/kobobridge/issues
|
|
8
|
+
Author: Younes Z.
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Younes Z.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: audiobookshelf,ebook,epub,kobo,selfhosted,sync
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Environment :: Web Environment
|
|
34
|
+
Classifier: Framework :: Flask
|
|
35
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Requires-Dist: flask>=2.2
|
|
42
|
+
Requires-Dist: requests>=2.28
|
|
43
|
+
Requires-Dist: waitress>=2.1
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# kobobridge
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pip install kobobridge
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Serves the ebooks in your [Audiobookshelf](https://www.audiobookshelf.org/) library to a Kobo eReader over the reader's own sync protocol, so books arrive over wifi instead of over a USB cable.
|
|
55
|
+
|
|
56
|
+
Answers [audiobookshelf#3504](https://github.com/advplyr/audiobookshelf/issues/3504), where people have been asking for Kobo sync for ebooks.
|
|
57
|
+
|
|
58
|
+
**Not affiliated with Audiobookshelf, and not affiliated with Rakuten Kobo Inc.** This is an independent project. "Kobo" is a trademark of its owner and is used here only to say which device the software talks to.
|
|
59
|
+
|
|
60
|
+
## What it does
|
|
61
|
+
|
|
62
|
+
Your Audiobookshelf server already holds your epubs. Your eReader already knows how to sync a library over the network. kobobridge sits between the two and answers the sync protocol on behalf of your own server:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Kobo eReader -> kobobridge -> Audiobookshelf
|
|
66
|
+
(read only)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- New epubs in Audiobookshelf appear on the reader after a sync.
|
|
70
|
+
- Covers, authors, series and descriptions come across.
|
|
71
|
+
- Reading progress from the reader is stored by the bridge.
|
|
72
|
+
- Large libraries are sent in batches, so the first sync completes.
|
|
73
|
+
|
|
74
|
+
## What it does not do
|
|
75
|
+
|
|
76
|
+
Worth reading before you install.
|
|
77
|
+
|
|
78
|
+
- **It never writes to your library.** Every call to Audiobookshelf is a GET. Reading progress the reader pushes is kept in a small file next to the bridge, not pushed into Audiobookshelf, so progress does not yet show up in the Audiobookshelf web player. A test asserts this.
|
|
79
|
+
- **It does not touch any account on the manufacturer's store.** No store credential, no user key, no device id. The bridge issues its own token and never contacts the store.
|
|
80
|
+
- **It does not convert anything.** Only epub and kepub files are offered. Pdf, mobi and azw3 in your library are skipped rather than converted.
|
|
81
|
+
- **Audiobooks and podcasts are skipped.** The reader cannot play them.
|
|
82
|
+
- **Shelves and collections are not mirrored yet.**
|
|
83
|
+
- **No telemetry, no phoning home, no analytics.** Nothing about your library leaves your machine.
|
|
84
|
+
|
|
85
|
+
## Setup
|
|
86
|
+
|
|
87
|
+
### 1. Get an Audiobookshelf API token
|
|
88
|
+
|
|
89
|
+
In Audiobookshelf: Settings, Users, pick your user, then copy the API token.
|
|
90
|
+
|
|
91
|
+
### 2. Run the bridge
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
export KOBOBRIDGE_ABS_URL=http://192.168.1.10:13378
|
|
95
|
+
export KOBOBRIDGE_ABS_TOKEN=paste-your-token-here
|
|
96
|
+
kobobridge check
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The `check` command lists the ebooks the reader will be offered. If that looks right:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
kobobridge run
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
It prints the one line you need to copy.
|
|
106
|
+
|
|
107
|
+
### 3. Point the reader at the bridge
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Point the eReader at this bridge:
|
|
111
|
+
|
|
112
|
+
1. Plug the eReader into a computer over USB.
|
|
113
|
+
2. Open the hidden folder .kobo/Kobo/ and edit 'Kobo eReader.conf'.
|
|
114
|
+
3. Under [OneStoreServices], set the line below. Create that
|
|
115
|
+
section if it is not already there.
|
|
116
|
+
|
|
117
|
+
api_endpoint=http://192.168.1.10:8484/kobo/rG9x2K7pQm4vB1nT8sYw
|
|
118
|
+
|
|
119
|
+
4. Eject, then sync from the eReader.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Keep a copy of the original `api_endpoint` value. Putting it back returns the reader to normal.
|
|
123
|
+
|
|
124
|
+
To keep the same token across restarts, set `KOBOBRIDGE_DEVICE_TOKEN` to the value the bridge printed. Otherwise a new one is generated each run and the reader has to be pointed again.
|
|
125
|
+
|
|
126
|
+
## Docker
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
docker run -d --name kobobridge -p 8484:8484 \
|
|
130
|
+
-e KOBOBRIDGE_ABS_URL=http://192.168.1.10:13378 \
|
|
131
|
+
-e KOBOBRIDGE_ABS_TOKEN=paste-your-token-here \
|
|
132
|
+
-e KOBOBRIDGE_DEVICE_TOKEN=pick-a-long-random-string \
|
|
133
|
+
-v kobobridge-data:/data \
|
|
134
|
+
ghcr.io/rezarys/kobobridge:latest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Settings
|
|
138
|
+
|
|
139
|
+
Required:
|
|
140
|
+
|
|
141
|
+
- `KOBOBRIDGE_ABS_URL`: address of your Audiobookshelf server.
|
|
142
|
+
- `KOBOBRIDGE_ABS_TOKEN`: Audiobookshelf API token.
|
|
143
|
+
|
|
144
|
+
Optional:
|
|
145
|
+
|
|
146
|
+
- `KOBOBRIDGE_DEVICE_TOKEN`: pin the reader token instead of generating one per run.
|
|
147
|
+
- `KOBOBRIDGE_PUBLIC_URL`: the address the reader sees, when behind a reverse proxy.
|
|
148
|
+
- `KOBOBRIDGE_LIBRARY_ID`: sync one library instead of every book library.
|
|
149
|
+
- `KOBOBRIDGE_PORT`: port to bind, default 8484.
|
|
150
|
+
- `KOBOBRIDGE_TIMEOUT`: seconds to wait on Audiobookshelf, default 30.
|
|
151
|
+
|
|
152
|
+
## Security
|
|
153
|
+
|
|
154
|
+
The reader token is the only thing standing between the internet and your library, so:
|
|
155
|
+
|
|
156
|
+
- Keep the bridge on your home network, or behind a VPN such as Tailscale or Wireguard.
|
|
157
|
+
- If you do expose it, put it behind a reverse proxy with TLS and set `KOBOBRIDGE_PUBLIC_URL`. The reader sends the token in the URL, which means it lands in access logs.
|
|
158
|
+
- Pin `KOBOBRIDGE_DEVICE_TOKEN` to something long and random.
|
|
159
|
+
|
|
160
|
+
## Status
|
|
161
|
+
|
|
162
|
+
Version 0.1.0, first release. The shapes of the sync protocol were learned by reading the long standing [Calibre-Web](https://github.com/janeczku/calibre-web) implementation, which has served this protocol since 2019. Reading only: Calibre-Web is under the GPL, this project is under the MIT licence, and no code was copied from it. The Audiobookshelf side and the batching logic are covered by tests.
|
|
163
|
+
|
|
164
|
+
What has not been exercised yet is a full sync against a physical reader, because the author does not own one. If you try it, an issue saying what happened is the single most useful thing you can send, working or not.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
git clone https://github.com/Rezarys/kobobridge
|
|
170
|
+
cd kobobridge
|
|
171
|
+
pip install -e ".[dev]"
|
|
172
|
+
pytest
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT. Younes Z.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# kobobridge
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
pip install kobobridge
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Serves the ebooks in your [Audiobookshelf](https://www.audiobookshelf.org/) library to a Kobo eReader over the reader's own sync protocol, so books arrive over wifi instead of over a USB cable.
|
|
8
|
+
|
|
9
|
+
Answers [audiobookshelf#3504](https://github.com/advplyr/audiobookshelf/issues/3504), where people have been asking for Kobo sync for ebooks.
|
|
10
|
+
|
|
11
|
+
**Not affiliated with Audiobookshelf, and not affiliated with Rakuten Kobo Inc.** This is an independent project. "Kobo" is a trademark of its owner and is used here only to say which device the software talks to.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
Your Audiobookshelf server already holds your epubs. Your eReader already knows how to sync a library over the network. kobobridge sits between the two and answers the sync protocol on behalf of your own server:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Kobo eReader -> kobobridge -> Audiobookshelf
|
|
19
|
+
(read only)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- New epubs in Audiobookshelf appear on the reader after a sync.
|
|
23
|
+
- Covers, authors, series and descriptions come across.
|
|
24
|
+
- Reading progress from the reader is stored by the bridge.
|
|
25
|
+
- Large libraries are sent in batches, so the first sync completes.
|
|
26
|
+
|
|
27
|
+
## What it does not do
|
|
28
|
+
|
|
29
|
+
Worth reading before you install.
|
|
30
|
+
|
|
31
|
+
- **It never writes to your library.** Every call to Audiobookshelf is a GET. Reading progress the reader pushes is kept in a small file next to the bridge, not pushed into Audiobookshelf, so progress does not yet show up in the Audiobookshelf web player. A test asserts this.
|
|
32
|
+
- **It does not touch any account on the manufacturer's store.** No store credential, no user key, no device id. The bridge issues its own token and never contacts the store.
|
|
33
|
+
- **It does not convert anything.** Only epub and kepub files are offered. Pdf, mobi and azw3 in your library are skipped rather than converted.
|
|
34
|
+
- **Audiobooks and podcasts are skipped.** The reader cannot play them.
|
|
35
|
+
- **Shelves and collections are not mirrored yet.**
|
|
36
|
+
- **No telemetry, no phoning home, no analytics.** Nothing about your library leaves your machine.
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
### 1. Get an Audiobookshelf API token
|
|
41
|
+
|
|
42
|
+
In Audiobookshelf: Settings, Users, pick your user, then copy the API token.
|
|
43
|
+
|
|
44
|
+
### 2. Run the bridge
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
export KOBOBRIDGE_ABS_URL=http://192.168.1.10:13378
|
|
48
|
+
export KOBOBRIDGE_ABS_TOKEN=paste-your-token-here
|
|
49
|
+
kobobridge check
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `check` command lists the ebooks the reader will be offered. If that looks right:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
kobobridge run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It prints the one line you need to copy.
|
|
59
|
+
|
|
60
|
+
### 3. Point the reader at the bridge
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Point the eReader at this bridge:
|
|
64
|
+
|
|
65
|
+
1. Plug the eReader into a computer over USB.
|
|
66
|
+
2. Open the hidden folder .kobo/Kobo/ and edit 'Kobo eReader.conf'.
|
|
67
|
+
3. Under [OneStoreServices], set the line below. Create that
|
|
68
|
+
section if it is not already there.
|
|
69
|
+
|
|
70
|
+
api_endpoint=http://192.168.1.10:8484/kobo/rG9x2K7pQm4vB1nT8sYw
|
|
71
|
+
|
|
72
|
+
4. Eject, then sync from the eReader.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Keep a copy of the original `api_endpoint` value. Putting it back returns the reader to normal.
|
|
76
|
+
|
|
77
|
+
To keep the same token across restarts, set `KOBOBRIDGE_DEVICE_TOKEN` to the value the bridge printed. Otherwise a new one is generated each run and the reader has to be pointed again.
|
|
78
|
+
|
|
79
|
+
## Docker
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
docker run -d --name kobobridge -p 8484:8484 \
|
|
83
|
+
-e KOBOBRIDGE_ABS_URL=http://192.168.1.10:13378 \
|
|
84
|
+
-e KOBOBRIDGE_ABS_TOKEN=paste-your-token-here \
|
|
85
|
+
-e KOBOBRIDGE_DEVICE_TOKEN=pick-a-long-random-string \
|
|
86
|
+
-v kobobridge-data:/data \
|
|
87
|
+
ghcr.io/rezarys/kobobridge:latest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Settings
|
|
91
|
+
|
|
92
|
+
Required:
|
|
93
|
+
|
|
94
|
+
- `KOBOBRIDGE_ABS_URL`: address of your Audiobookshelf server.
|
|
95
|
+
- `KOBOBRIDGE_ABS_TOKEN`: Audiobookshelf API token.
|
|
96
|
+
|
|
97
|
+
Optional:
|
|
98
|
+
|
|
99
|
+
- `KOBOBRIDGE_DEVICE_TOKEN`: pin the reader token instead of generating one per run.
|
|
100
|
+
- `KOBOBRIDGE_PUBLIC_URL`: the address the reader sees, when behind a reverse proxy.
|
|
101
|
+
- `KOBOBRIDGE_LIBRARY_ID`: sync one library instead of every book library.
|
|
102
|
+
- `KOBOBRIDGE_PORT`: port to bind, default 8484.
|
|
103
|
+
- `KOBOBRIDGE_TIMEOUT`: seconds to wait on Audiobookshelf, default 30.
|
|
104
|
+
|
|
105
|
+
## Security
|
|
106
|
+
|
|
107
|
+
The reader token is the only thing standing between the internet and your library, so:
|
|
108
|
+
|
|
109
|
+
- Keep the bridge on your home network, or behind a VPN such as Tailscale or Wireguard.
|
|
110
|
+
- If you do expose it, put it behind a reverse proxy with TLS and set `KOBOBRIDGE_PUBLIC_URL`. The reader sends the token in the URL, which means it lands in access logs.
|
|
111
|
+
- Pin `KOBOBRIDGE_DEVICE_TOKEN` to something long and random.
|
|
112
|
+
|
|
113
|
+
## Status
|
|
114
|
+
|
|
115
|
+
Version 0.1.0, first release. The shapes of the sync protocol were learned by reading the long standing [Calibre-Web](https://github.com/janeczku/calibre-web) implementation, which has served this protocol since 2019. Reading only: Calibre-Web is under the GPL, this project is under the MIT licence, and no code was copied from it. The Audiobookshelf side and the batching logic are covered by tests.
|
|
116
|
+
|
|
117
|
+
What has not been exercised yet is a full sync against a physical reader, because the author does not own one. If you try it, an issue saying what happened is the single most useful thing you can send, working or not.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
git clone https://github.com/Rezarys/kobobridge
|
|
123
|
+
cd kobobridge
|
|
124
|
+
pip install -e ".[dev]"
|
|
125
|
+
pytest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT. Younes Z.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kobobridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Serve an Audiobookshelf ebook library to a Kobo eReader over the native sync protocol."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Younes Z." }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = [
|
|
14
|
+
"kobo",
|
|
15
|
+
"audiobookshelf",
|
|
16
|
+
"ebook",
|
|
17
|
+
"epub",
|
|
18
|
+
"selfhosted",
|
|
19
|
+
"sync",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Environment :: Web Environment",
|
|
24
|
+
"Framework :: Flask",
|
|
25
|
+
"Intended Audience :: End Users/Desktop",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"flask>=2.2",
|
|
33
|
+
"requests>=2.28",
|
|
34
|
+
"waitress>=2.1",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=7.0"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
kobobridge = "kobobridge.__main__:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/Rezarys/kobobridge"
|
|
45
|
+
Source = "https://github.com/Rezarys/kobobridge"
|
|
46
|
+
Issues = "https://github.com/Rezarys/kobobridge/issues"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/kobobridge"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Serve an Audiobookshelf ebook library to a Kobo eReader over the native sync protocol."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
__all__ = ["__version__", "create_app"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_app(config=None):
|
|
9
|
+
"""Build the WSGI application. Imported lazily to keep ``import kobobridge`` cheap."""
|
|
10
|
+
from .app import create_app as _create_app
|
|
11
|
+
|
|
12
|
+
return _create_app(config)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""The command line: check the connection, print the setup line, run the server."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .abs import AudiobookshelfError
|
|
9
|
+
from .config import Config, ConfigError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_config():
|
|
13
|
+
try:
|
|
14
|
+
return Config.from_env()
|
|
15
|
+
except ConfigError as error:
|
|
16
|
+
sys.stderr.write("kobobridge: {0}\n".format(error))
|
|
17
|
+
raise SystemExit(2)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _endpoint(config, host, port):
|
|
21
|
+
if config.public_url:
|
|
22
|
+
return "{0}/kobo/{1}".format(config.public_url, config.device_token)
|
|
23
|
+
shown = "127.0.0.1" if host in ("0.0.0.0", "::", "") else host
|
|
24
|
+
return "http://{0}:{1}/kobo/{2}".format(shown, port, config.device_token)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _print_setup(config, host, port):
|
|
28
|
+
endpoint = _endpoint(config, host, port)
|
|
29
|
+
print("")
|
|
30
|
+
print("Point the eReader at this bridge:")
|
|
31
|
+
print("")
|
|
32
|
+
print(" 1. Plug the eReader into a computer over USB.")
|
|
33
|
+
print(" 2. Open the hidden folder .kobo/Kobo/ and edit 'Kobo eReader.conf'.")
|
|
34
|
+
print(" 3. Under [OneStoreServices], set:")
|
|
35
|
+
print("")
|
|
36
|
+
print(" api_endpoint={0}".format(endpoint))
|
|
37
|
+
print("")
|
|
38
|
+
print(" 4. Eject, then sync from the eReader.")
|
|
39
|
+
print("")
|
|
40
|
+
if not os.environ.get("KOBOBRIDGE_DEVICE_TOKEN"):
|
|
41
|
+
print("This token was generated for this run only. To keep it across restarts, set:")
|
|
42
|
+
print("")
|
|
43
|
+
print(" KOBOBRIDGE_DEVICE_TOKEN={0}".format(config.device_token))
|
|
44
|
+
print("")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_check(args):
|
|
48
|
+
config = _load_config()
|
|
49
|
+
from .app import Bridge
|
|
50
|
+
|
|
51
|
+
bridge = Bridge(config)
|
|
52
|
+
try:
|
|
53
|
+
count = bridge.client.ping()
|
|
54
|
+
except AudiobookshelfError as error:
|
|
55
|
+
sys.stderr.write("kobobridge: {0}\n".format(error))
|
|
56
|
+
return 1
|
|
57
|
+
print("Audiobookshelf answered: {0} librar{1}.".format(count, "y" if count == 1 else "ies"))
|
|
58
|
+
try:
|
|
59
|
+
books = bridge.refresh(force=True)
|
|
60
|
+
except AudiobookshelfError as error:
|
|
61
|
+
sys.stderr.write("kobobridge: {0}\n".format(error))
|
|
62
|
+
return 1
|
|
63
|
+
print("Ebooks the eReader can read: {0}.".format(len(books)))
|
|
64
|
+
for book in books[: args.limit]:
|
|
65
|
+
author = ", ".join(book.authors) or "unknown author"
|
|
66
|
+
print(" {0} ({1}) [{2}]".format(book.title, author, book.ebook_format))
|
|
67
|
+
if len(books) > args.limit:
|
|
68
|
+
print(" and {0} more.".format(len(books) - args.limit))
|
|
69
|
+
if not books:
|
|
70
|
+
print("")
|
|
71
|
+
print("No epub found. Audiobookshelf only reports one when the library item has an")
|
|
72
|
+
print("ebook file; audiobooks and podcasts are skipped on purpose.")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_setup(args):
|
|
77
|
+
config = _load_config()
|
|
78
|
+
_print_setup(config, args.host, args.port)
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_run(args):
|
|
83
|
+
config = _load_config()
|
|
84
|
+
from .app import create_app
|
|
85
|
+
from waitress import serve
|
|
86
|
+
|
|
87
|
+
app = create_app(config)
|
|
88
|
+
_print_setup(config, args.host, args.port)
|
|
89
|
+
print("Listening on {0}:{1}. Stop with Ctrl+C.".format(args.host, args.port))
|
|
90
|
+
serve(app, host=args.host, port=args.port, threads=args.threads)
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_parser():
|
|
95
|
+
parser = argparse.ArgumentParser(
|
|
96
|
+
prog="kobobridge",
|
|
97
|
+
description="Serve an Audiobookshelf ebook library to a Kobo eReader.",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
100
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
101
|
+
|
|
102
|
+
def with_address(sub):
|
|
103
|
+
sub.add_argument("--host", default="0.0.0.0", help="address to bind, default 0.0.0.0")
|
|
104
|
+
sub.add_argument(
|
|
105
|
+
"--port", type=int, default=int(os.environ.get("KOBOBRIDGE_PORT", 8484)),
|
|
106
|
+
help="port to bind, default 8484",
|
|
107
|
+
)
|
|
108
|
+
return sub
|
|
109
|
+
|
|
110
|
+
run = with_address(subparsers.add_parser("run", help="run the bridge"))
|
|
111
|
+
run.add_argument("--threads", type=int, default=8, help="worker threads, default 8")
|
|
112
|
+
run.set_defaults(func=cmd_run)
|
|
113
|
+
|
|
114
|
+
check = subparsers.add_parser("check", help="test the Audiobookshelf connection")
|
|
115
|
+
check.add_argument("--limit", type=int, default=10, help="how many titles to list")
|
|
116
|
+
check.set_defaults(func=cmd_check)
|
|
117
|
+
|
|
118
|
+
setup = with_address(subparsers.add_parser("setup", help="print the eReader setup line"))
|
|
119
|
+
setup.set_defaults(func=cmd_setup)
|
|
120
|
+
return parser
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv=None):
|
|
124
|
+
parser = build_parser()
|
|
125
|
+
args = parser.parse_args(argv)
|
|
126
|
+
if not getattr(args, "func", None):
|
|
127
|
+
parser.print_help()
|
|
128
|
+
return 1
|
|
129
|
+
return args.func(args)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
raise SystemExit(main())
|