pytolino 1.6__py3-none-any.whl → 2.0__py3-none-any.whl
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.
- pytolino/tolino_cloud.py +176 -306
- {pytolino-1.6.dist-info → pytolino-2.0.dist-info}/METADATA +40 -35
- pytolino-2.0.dist-info/RECORD +7 -0
- {pytolino-1.6.dist-info → pytolino-2.0.dist-info}/WHEEL +1 -1
- pytolino/servers_settings.ini +0 -29
- pytolino-1.6.dist-info/RECORD +0 -8
- {pytolino-1.6.dist-info → pytolino-2.0.dist-info}/licenses/LICENSE +0 -0
- {pytolino-1.6.dist-info → pytolino-2.0.dist-info}/top_level.txt +0 -0
pytolino/tolino_cloud.py
CHANGED
|
@@ -1,310 +1,182 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
import os
|
|
5
|
-
import configparser
|
|
6
|
-
import platform
|
|
7
4
|
import logging
|
|
8
|
-
from urllib.parse import urlparse, parse_qs
|
|
9
|
-
from urllib3.util import Retry
|
|
10
5
|
import json
|
|
11
6
|
import time
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
import requests
|
|
15
|
-
|
|
16
|
-
import
|
|
12
|
+
import curl_cffi
|
|
13
|
+
from varboxes import VarBox
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
class PytolinoException(Exception):
|
|
20
17
|
pass
|
|
21
18
|
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
SERVERS_SETTINGS_FN,
|
|
27
|
-
)
|
|
28
|
-
servers_settings = configparser.ConfigParser()
|
|
29
|
-
servers_settings.read(SERVERS_SETTINGS_FILE_PATH)
|
|
20
|
+
class ExpirationError(PytolinoException):
|
|
21
|
+
pass
|
|
22
|
+
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
SERVERS_SETTINGS_FN = 'servers_settings.toml'
|
|
25
|
+
SERVERS_SETTINGS_FP = Path(__file__).parent / SERVERS_SETTINGS_FN
|
|
26
|
+
servers_settings = tomllib.loads(SERVERS_SETTINGS_FP.read_text())
|
|
27
|
+
PARTNERS = servers_settings.keys()
|
|
34
28
|
|
|
35
29
|
|
|
36
30
|
def main():
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
for partner in PARTNERS:
|
|
32
|
+
print(partner)
|
|
33
|
+
for key, val in servers_settings[partner].items():
|
|
34
|
+
print(key, val)
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
class Client(object):
|
|
42
38
|
|
|
43
39
|
"""create a client to communicate with a tolino partner (login, etc..)"""
|
|
44
40
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# 33 = browser id
|
|
73
|
-
# 44 = browser version
|
|
74
|
-
# X = the result of a fingerprinting image
|
|
75
|
-
|
|
76
|
-
os_id = {
|
|
77
|
-
'Windows': '1',
|
|
78
|
-
'Darwin': '2',
|
|
79
|
-
'Linux': '3'
|
|
80
|
-
}.get(platform.system(), 'x')
|
|
81
|
-
|
|
82
|
-
# The hardware id contains some info about the browser
|
|
83
|
-
#
|
|
84
|
-
# Hey, tolino developers: Let me know which id values to use here
|
|
85
|
-
engine_id = 'x'
|
|
86
|
-
browser_id = 'xx'
|
|
87
|
-
version_id = '00'
|
|
88
|
-
|
|
89
|
-
# For some odd reason, the tolino javascript draws the text
|
|
90
|
-
# "www.tolino.de" and a rectangle filled with the offical Telekom
|
|
91
|
-
# magenta #E20074 (http://de.wikipedia.org/wiki/Magenta_%28Farbe%29)
|
|
92
|
-
# into an image canvas and then fuddles around with the
|
|
93
|
-
# base64-encoded PNG. Probably to gain some sort of fingerprint,
|
|
94
|
-
# but it's not quite clear how this would help the tolino API.
|
|
95
|
-
#
|
|
96
|
-
# Hey, tolino developers: Let me know what you need here.
|
|
97
|
-
|
|
98
|
-
fingerprint = 'ABCDEFGHIJKLMNOPQR'
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
os_id +
|
|
102
|
-
engine_id +
|
|
103
|
-
browser_id +
|
|
104
|
-
fingerprint[0:1] +
|
|
105
|
-
'-' +
|
|
106
|
-
version_id +
|
|
107
|
-
fingerprint[1:4] +
|
|
108
|
-
'-' +
|
|
109
|
-
fingerprint[4:9] +
|
|
110
|
-
'-' +
|
|
111
|
-
fingerprint[9:14] +
|
|
112
|
-
'-' +
|
|
113
|
-
fingerprint[14:18] +
|
|
114
|
-
'h'
|
|
115
|
-
)
|
|
41
|
+
def store_token(
|
|
42
|
+
account_name,
|
|
43
|
+
refresh_token: str,
|
|
44
|
+
expires_in: int,
|
|
45
|
+
hardward_id: str,
|
|
46
|
+
access_token='',
|
|
47
|
+
):
|
|
48
|
+
"""after one has connected in a browser,
|
|
49
|
+
one can store the token for the app.
|
|
50
|
+
|
|
51
|
+
:account_name: internal name for reference
|
|
52
|
+
:refresh_token: given by server after a token POST request
|
|
53
|
+
:expires_in: time in seconds
|
|
54
|
+
:hardward_id: present in payload for every request to API.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
vb = VarBox('pytolino', app_name=account_name)
|
|
58
|
+
vb.refresh_token = refresh_token
|
|
59
|
+
vb.hardware_id = hardward_id
|
|
60
|
+
vb.expires_in = expires_in
|
|
61
|
+
vb.timestamp = time.time()
|
|
62
|
+
vb.access_token = access_token
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def refresh_token(self) -> str:
|
|
66
|
+
"""refresh token to get new access token"""
|
|
67
|
+
return self._refresh_token
|
|
116
68
|
|
|
117
|
-
|
|
69
|
+
@property
|
|
70
|
+
def hardware_id(self) -> str:
|
|
71
|
+
"""hardware id that is sent in request payloads"""
|
|
72
|
+
return self._hardware_id
|
|
118
73
|
|
|
119
|
-
def __init__(self, server_name='
|
|
74
|
+
def __init__(self, server_name='orellfuessli'):
|
|
120
75
|
|
|
121
76
|
if server_name not in servers_settings:
|
|
122
77
|
raise PytolinoException(
|
|
123
78
|
f'the partner {server_name} was not found.'
|
|
124
79
|
f'please choose one of the list: {PARTNERS}')
|
|
125
80
|
|
|
126
|
-
self.
|
|
127
|
-
self.
|
|
128
|
-
self.
|
|
129
|
-
|
|
130
|
-
self.server_settings = servers_settings[server_name]
|
|
131
|
-
self.session = requests.Session()
|
|
132
|
-
retry_strategy = Retry(
|
|
133
|
-
total=TOTAL_RETRY,
|
|
134
|
-
status_forcelist=STATUS_FORCELIST,
|
|
135
|
-
backoff_factor=2,
|
|
136
|
-
allowed_methods=frozenset(['GET', 'POST']))
|
|
137
|
-
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
138
|
-
self.session.mount('http://', adapter)
|
|
139
|
-
self.session.mount('https://', adapter)
|
|
140
|
-
self.browser = mechanize.Browser()
|
|
141
|
-
self.browser.set_handle_robots(False)
|
|
142
|
-
self.server_name = server_name
|
|
143
|
-
|
|
144
|
-
def login(self, username, password):
|
|
145
|
-
"""login to the partner and get access token.
|
|
81
|
+
self._access_token = None
|
|
82
|
+
self._refresh_token = None
|
|
83
|
+
self._token_expires = None
|
|
84
|
+
self._hardware_id = None
|
|
146
85
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
logging.info(f'login to {self.server_name}...')
|
|
86
|
+
self._server_settings = servers_settings[server_name]
|
|
87
|
+
self._session = requests.Session()
|
|
88
|
+
self._session_cffi = curl_cffi.Session()
|
|
89
|
+
self._server_name = server_name
|
|
152
90
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
91
|
+
def retrieve_token(
|
|
92
|
+
self,
|
|
93
|
+
account_name,
|
|
94
|
+
) -> tuple[str, str]:
|
|
95
|
+
"""get the token and data that were stored previousely.
|
|
96
|
+
raise error if expired
|
|
158
97
|
|
|
159
|
-
|
|
160
|
-
|
|
98
|
+
:account_name: internal name under which token was stored
|
|
99
|
+
:returns: refresh_token, hardware_id
|
|
161
100
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if not
|
|
165
|
-
raise PytolinoException(
|
|
101
|
+
"""
|
|
102
|
+
vb = VarBox('pytolino', app_name=account_name)
|
|
103
|
+
if not hasattr(vb, 'refresh_token'):
|
|
104
|
+
raise PytolinoException(
|
|
105
|
+
'there was no refresh token stored for that name')
|
|
106
|
+
now = time.time()
|
|
107
|
+
expiration_time = vb.timestamp + vb.expires_in
|
|
108
|
+
if now > expiration_time:
|
|
109
|
+
raise ExpirationError('the refresh token has expired')
|
|
110
|
+
else:
|
|
111
|
+
self._refresh_token = vb.refresh_token
|
|
112
|
+
self._hardware_id = vb.hardware_id
|
|
113
|
+
self._access_token = vb.access_token
|
|
166
114
|
|
|
167
|
-
|
|
115
|
+
def get_new_token(self, account_name):
|
|
116
|
+
"""look at the store token, and get a new access and refresh tokens.
|
|
168
117
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
'response_type': 'code',
|
|
172
|
-
'scope': self.server_settings['scope'],
|
|
173
|
-
'redirect_uri': self.server_settings['reader_url']
|
|
174
|
-
}
|
|
175
|
-
if 'login_form_url' in self.server_settings:
|
|
176
|
-
params['x_buchde.skin_id'] = self.server_settings[
|
|
177
|
-
'x_buchde.skin_id']
|
|
178
|
-
params['x_buchde.mandant_id'] = self.server_settings[
|
|
179
|
-
'x_buchde.mandant_id']
|
|
180
|
-
host_response = self.session.get(
|
|
181
|
-
self.server_settings['auth_url'],
|
|
182
|
-
params=params,
|
|
183
|
-
verify=True,
|
|
184
|
-
allow_redirects=False,
|
|
185
|
-
)
|
|
118
|
+
:account_name: TODO
|
|
119
|
+
:returns: TODO
|
|
186
120
|
|
|
187
|
-
|
|
121
|
+
"""
|
|
122
|
+
self.retrieve_token(account_name)
|
|
188
123
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
host_response = self.
|
|
199
|
-
self.
|
|
200
|
-
data=
|
|
201
|
-
'client_id': self.server_settings['client_id'],
|
|
202
|
-
'grant_type': 'authorization_code',
|
|
203
|
-
'code': auth_code,
|
|
204
|
-
'scope': self.server_settings['scope'],
|
|
205
|
-
'redirect_uri': self.server_settings['reader_url']
|
|
206
|
-
},
|
|
124
|
+
headers = {
|
|
125
|
+
'Referer': 'https://webreader.mytolino.com/',
|
|
126
|
+
}
|
|
127
|
+
payload = {
|
|
128
|
+
'client_id': 'webreader',
|
|
129
|
+
'grant_type': 'refresh_token',
|
|
130
|
+
'refresh_token': self.refresh_token,
|
|
131
|
+
'scope': 'SCOPE_BOSH',
|
|
132
|
+
}
|
|
133
|
+
host_response = self._session_cffi.post(
|
|
134
|
+
self._server_settings['token_url'],
|
|
135
|
+
data=payload,
|
|
207
136
|
verify=True,
|
|
208
|
-
allow_redirects=
|
|
137
|
+
allow_redirects=True,
|
|
138
|
+
headers=headers,
|
|
139
|
+
impersonate='chrome',
|
|
209
140
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
141
|
+
if not host_response.ok:
|
|
142
|
+
msg = str(host_response)
|
|
143
|
+
logging.error('failed to get a new token')
|
|
144
|
+
logging.error(msg)
|
|
145
|
+
raise PytolinoException('failed to get a new token')
|
|
146
|
+
else:
|
|
213
147
|
j = host_response.json()
|
|
214
|
-
self.
|
|
215
|
-
self.
|
|
216
|
-
self.
|
|
217
|
-
|
|
218
|
-
|
|
148
|
+
self._access_token = j['access_token']
|
|
149
|
+
self._refresh_token = j['refresh_token']
|
|
150
|
+
self._token_expires = int(j['expires_in'])
|
|
151
|
+
Client.store_token(
|
|
152
|
+
account_name,
|
|
153
|
+
self.refresh_token,
|
|
154
|
+
self._token_expires,
|
|
155
|
+
self.hardware_id,
|
|
156
|
+
access_token=self._access_token,
|
|
157
|
+
)
|
|
158
|
+
logging.info('got a new access token!')
|
|
159
|
+
|
|
160
|
+
def login(self, username, password, fp=None):
|
|
161
|
+
"""login to the partner and get access token.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
msg = 'login does not work anymore because of bot protection'
|
|
165
|
+
'connect manualy (once) and use store_token and retrieve token'
|
|
166
|
+
'methods instead'
|
|
167
|
+
raise NotImplementedError(msg)
|
|
219
168
|
|
|
220
169
|
def logout(self):
|
|
221
170
|
"""logout from tolino partner host
|
|
222
171
|
|
|
223
172
|
"""
|
|
224
|
-
|
|
225
|
-
host_response = self.session.post(
|
|
226
|
-
self.server_settings['revoke_url'],
|
|
227
|
-
data={
|
|
228
|
-
'client_id': self.server_settings['client_id'],
|
|
229
|
-
'token_type': 'refresh_token',
|
|
230
|
-
'token': self.refresh_token,
|
|
231
|
-
}
|
|
232
|
-
)
|
|
233
|
-
self._log_requests(host_response)
|
|
234
|
-
if host_response.status_code != 200:
|
|
235
|
-
raise PytolinoException('logout failed.')
|
|
236
|
-
else:
|
|
237
|
-
host_response = self.session.post(
|
|
238
|
-
self.server_settings['logout_url'],
|
|
239
|
-
)
|
|
240
|
-
self._log_requests(host_response)
|
|
241
|
-
if host_response.status_code != 200:
|
|
242
|
-
raise PytolinoException('logout failed.')
|
|
173
|
+
raise NotImplementedError('logout is not necessary with tokens')
|
|
243
174
|
|
|
244
175
|
def register(self):
|
|
245
|
-
|
|
246
|
-
upload files. you need to login first.
|
|
247
|
-
|
|
248
|
-
"""
|
|
249
|
-
host_response = self.session.post(
|
|
250
|
-
self.server_settings['register_url'],
|
|
251
|
-
data=json.dumps({'hardware_name': 'tolino sync reader'}),
|
|
252
|
-
headers={
|
|
253
|
-
'content-type': 'application/json',
|
|
254
|
-
't_auth_token': self.access_token,
|
|
255
|
-
'hardware_id': self.hardware_id,
|
|
256
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
257
|
-
'client_type': 'TOLINO_WEBREADER',
|
|
258
|
-
'client_version': '4.4.1',
|
|
259
|
-
'hardware_type': 'HTML5',
|
|
260
|
-
}
|
|
261
|
-
)
|
|
262
|
-
self._log_requests(host_response)
|
|
263
|
-
if host_response.status_code != 200:
|
|
264
|
-
raise PytolinoException(f'register {self.hardware_id} failed.')
|
|
176
|
+
raise NotImplementedError('register is not necessary with tokens')
|
|
265
177
|
|
|
266
178
|
def unregister(self, device_id=None):
|
|
267
|
-
|
|
268
|
-
it is assumed the device in use will be removed
|
|
269
|
-
|
|
270
|
-
:device_id: None or str if we want to unregister another device
|
|
271
|
-
:returns: None
|
|
272
|
-
|
|
273
|
-
"""
|
|
274
|
-
if device_id is None:
|
|
275
|
-
device_id = self.hardware_id
|
|
276
|
-
|
|
277
|
-
host_response = self.session.post(
|
|
278
|
-
self.server_settings['unregister_url'],
|
|
279
|
-
data=json.dumps({
|
|
280
|
-
'deleteDevicesRequest': {
|
|
281
|
-
'accounts': [{
|
|
282
|
-
'auth_token': self.access_token,
|
|
283
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
284
|
-
}],
|
|
285
|
-
'devices': [{
|
|
286
|
-
'device_id': device_id,
|
|
287
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
288
|
-
}]
|
|
289
|
-
}
|
|
290
|
-
}),
|
|
291
|
-
headers={
|
|
292
|
-
'content-type': 'application/json',
|
|
293
|
-
't_auth_token': self.access_token,
|
|
294
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
295
|
-
}
|
|
296
|
-
)
|
|
297
|
-
self._log_requests(host_response)
|
|
298
|
-
if host_response.status_code != 200:
|
|
299
|
-
try:
|
|
300
|
-
j = host_response.json()
|
|
301
|
-
raise PytolinoException(
|
|
302
|
-
f"unregister {device_id} failed: ",
|
|
303
|
-
f"{j['ResponseInfo']['message']}"
|
|
304
|
-
)
|
|
305
|
-
except KeyError:
|
|
306
|
-
raise PytolinoException(
|
|
307
|
-
f'unregister {device_id} failed: reason unknown.')
|
|
179
|
+
raise NotImplementedError('unregister is not necessary with tokens')
|
|
308
180
|
|
|
309
181
|
def get_inventory(self):
|
|
310
182
|
"""download a list of the books on the cloud and their information
|
|
@@ -312,19 +184,19 @@ class Client(object):
|
|
|
312
184
|
|
|
313
185
|
"""
|
|
314
186
|
|
|
315
|
-
host_response = self.
|
|
316
|
-
self.
|
|
187
|
+
host_response = self._session.get(
|
|
188
|
+
self._server_settings['inventory_url'],
|
|
317
189
|
params={'strip': 'true'},
|
|
318
190
|
headers={
|
|
319
|
-
't_auth_token': self.
|
|
320
|
-
'hardware_id': self.
|
|
321
|
-
'reseller_id': self.
|
|
191
|
+
't_auth_token': self._access_token,
|
|
192
|
+
'hardware_id': self._hardware_id,
|
|
193
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
322
194
|
}
|
|
323
195
|
)
|
|
324
196
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
197
|
+
if not host_response.ok:
|
|
198
|
+
raise PytolinoException(
|
|
199
|
+
f'inventory request failed {host_response}')
|
|
328
200
|
|
|
329
201
|
try:
|
|
330
202
|
j = host_response.json()
|
|
@@ -367,20 +239,21 @@ class Client(object):
|
|
|
367
239
|
}]
|
|
368
240
|
}
|
|
369
241
|
|
|
370
|
-
host_response = self.
|
|
371
|
-
self.
|
|
242
|
+
host_response = self._session.patch(
|
|
243
|
+
self._server_settings['sync_data_url'],
|
|
372
244
|
data=json.dumps(payload),
|
|
373
245
|
headers={
|
|
374
246
|
'content-type': 'application/json',
|
|
375
|
-
't_auth_token': self.
|
|
247
|
+
't_auth_token': self._access_token,
|
|
376
248
|
'hardware_id': self.hardware_id,
|
|
377
|
-
'reseller_id': self.
|
|
249
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
378
250
|
'client_type': 'TOLINO_WEBREADER',
|
|
379
251
|
}
|
|
380
252
|
)
|
|
381
|
-
|
|
382
|
-
if host_response.
|
|
383
|
-
raise PytolinoException(
|
|
253
|
+
|
|
254
|
+
if not host_response.ok:
|
|
255
|
+
raise PytolinoException(
|
|
256
|
+
f'collection add failed {host_response}')
|
|
384
257
|
|
|
385
258
|
def upload_metadata(self, book_id, **new_metadata):
|
|
386
259
|
"""upload some metadata to a specific book on the cloud
|
|
@@ -390,20 +263,19 @@ class Client(object):
|
|
|
390
263
|
|
|
391
264
|
"""
|
|
392
265
|
|
|
393
|
-
url = self.
|
|
394
|
-
host_response = self.
|
|
266
|
+
url = self._server_settings['meta_url'] + f'/?deliverableId={book_id}'
|
|
267
|
+
host_response = self._session.get(
|
|
395
268
|
url,
|
|
396
269
|
headers={
|
|
397
|
-
't_auth_token': self.
|
|
270
|
+
't_auth_token': self._access_token,
|
|
398
271
|
'hardware_id': self.hardware_id,
|
|
399
|
-
'reseller_id': self.
|
|
272
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
400
273
|
}
|
|
401
274
|
)
|
|
402
275
|
|
|
403
276
|
book = host_response.json()
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
raise PytolinoException('metadata upload failed')
|
|
277
|
+
if not host_response.ok:
|
|
278
|
+
raise PytolinoException(f'metadata upload failed {host_response}')
|
|
407
279
|
|
|
408
280
|
for key, value in new_metadata.items():
|
|
409
281
|
book['metadata'][key] = value
|
|
@@ -412,20 +284,19 @@ class Client(object):
|
|
|
412
284
|
'uploadMetaData': book['metadata']
|
|
413
285
|
}
|
|
414
286
|
|
|
415
|
-
host_response = self.
|
|
287
|
+
host_response = self._session.put(
|
|
416
288
|
url,
|
|
417
289
|
data=json.dumps(payload),
|
|
418
290
|
headers={
|
|
419
291
|
'content-type': 'application/json',
|
|
420
|
-
't_auth_token': self.
|
|
292
|
+
't_auth_token': self._access_token,
|
|
421
293
|
'hardware_id': self.hardware_id,
|
|
422
|
-
'reseller_id': self.
|
|
294
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
423
295
|
}
|
|
424
296
|
)
|
|
425
297
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
raise PytolinoException('metadata upload failed')
|
|
298
|
+
if not host_response.ok:
|
|
299
|
+
raise PytolinoException(f'metadata upload failed {host_response}')
|
|
429
300
|
|
|
430
301
|
def upload(self, file_path, name=None, extension=None):
|
|
431
302
|
"""upload an ebook to your cloud
|
|
@@ -447,8 +318,8 @@ class Client(object):
|
|
|
447
318
|
'epub': 'application/epub+zip',
|
|
448
319
|
}.get(extension.lower(), 'application/pdf')
|
|
449
320
|
|
|
450
|
-
host_response = self.
|
|
451
|
-
self.
|
|
321
|
+
host_response = self._session.post(
|
|
322
|
+
self._server_settings['upload_url'],
|
|
452
323
|
files=[(
|
|
453
324
|
'file',
|
|
454
325
|
(
|
|
@@ -458,14 +329,15 @@ class Client(object):
|
|
|
458
329
|
),
|
|
459
330
|
)],
|
|
460
331
|
headers={
|
|
461
|
-
't_auth_token': self.
|
|
332
|
+
't_auth_token': self._access_token,
|
|
462
333
|
'hardware_id': self.hardware_id,
|
|
463
|
-
'reseller_id': self.
|
|
334
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
464
335
|
}
|
|
465
336
|
)
|
|
466
|
-
|
|
467
|
-
if host_response.
|
|
468
|
-
raise PytolinoException('
|
|
337
|
+
|
|
338
|
+
if not host_response.ok:
|
|
339
|
+
raise PytolinoException(f'upload failed {host_response}')
|
|
340
|
+
|
|
469
341
|
try:
|
|
470
342
|
j = host_response.json()
|
|
471
343
|
except requests.JSONDecodeError:
|
|
@@ -483,18 +355,17 @@ class Client(object):
|
|
|
483
355
|
:returns: None
|
|
484
356
|
|
|
485
357
|
"""
|
|
486
|
-
host_response = self.
|
|
487
|
-
self.
|
|
358
|
+
host_response = self._session.get(
|
|
359
|
+
self._server_settings['delete_url'],
|
|
488
360
|
params={'deliverableId': ebook_id},
|
|
489
361
|
headers={
|
|
490
|
-
't_auth_token': self.
|
|
362
|
+
't_auth_token': self._access_token,
|
|
491
363
|
'hardware_id': self.hardware_id,
|
|
492
|
-
'reseller_id': self.
|
|
364
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
493
365
|
}
|
|
494
366
|
)
|
|
495
|
-
self._log_requests(host_response)
|
|
496
367
|
|
|
497
|
-
if host_response.
|
|
368
|
+
if not host_response.ok:
|
|
498
369
|
try:
|
|
499
370
|
j = host_response.json()
|
|
500
371
|
raise PytolinoException(
|
|
@@ -510,7 +381,8 @@ class Client(object):
|
|
|
510
381
|
|
|
511
382
|
:book_id: id of the book on the serveer
|
|
512
383
|
:filepath: path to the cover file
|
|
513
|
-
:file_ext: png, jpg or jpeg. only necessary if the
|
|
384
|
+
:file_ext: png, jpg or jpeg. only necessary if the
|
|
385
|
+
filepath has no extension
|
|
514
386
|
|
|
515
387
|
"""
|
|
516
388
|
|
|
@@ -523,21 +395,19 @@ class Client(object):
|
|
|
523
395
|
'jpg': 'image/jpeg'
|
|
524
396
|
}.get(ext.lower(), 'application/jpeg')
|
|
525
397
|
|
|
526
|
-
host_response = self.
|
|
527
|
-
self.
|
|
528
|
-
files
|
|
529
|
-
data
|
|
398
|
+
host_response = self._session.post(
|
|
399
|
+
self._server_settings['cover_url'],
|
|
400
|
+
files=[('file', ('1092560016', open(filepath, 'rb'), mime))],
|
|
401
|
+
data={'deliverableId': book_id},
|
|
530
402
|
headers={
|
|
531
|
-
't_auth_token': self.
|
|
403
|
+
't_auth_token': self._access_token,
|
|
532
404
|
'hardware_id': self.hardware_id,
|
|
533
|
-
'reseller_id': self.
|
|
405
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
534
406
|
},
|
|
535
407
|
)
|
|
536
408
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if host_response.status_code != 200:
|
|
540
|
-
raise PytolinoException('cover upload failed.')
|
|
409
|
+
if not host_response.ok:
|
|
410
|
+
raise PytolinoException(f'cover upload failed. {host_response}')
|
|
541
411
|
|
|
542
412
|
|
|
543
413
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytolino
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0
|
|
4
4
|
Summary: client for tolino cloud
|
|
5
5
|
Author: Imam Usmani
|
|
6
6
|
Project-URL: Source Code, https://github.com/ImamAzim/pytolino
|
|
@@ -15,6 +15,8 @@ Description-Content-Type: text/x-rst
|
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: requests
|
|
17
17
|
Requires-Dist: mechanize
|
|
18
|
+
Requires-Dist: curl_cffi
|
|
19
|
+
Requires-Dist: varboxes
|
|
18
20
|
Provides-Extra: dev
|
|
19
21
|
Requires-Dist: pytest; extra == "dev"
|
|
20
22
|
Requires-Dist: flake8; extra == "dev"
|
|
@@ -25,10 +27,16 @@ Requires-Dist: twine; extra == "dev"
|
|
|
25
27
|
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
26
28
|
Dynamic: license-file
|
|
27
29
|
|
|
30
|
+
UPDATE
|
|
31
|
+
===========
|
|
32
|
+
|
|
33
|
+
because of heavy anti-bot protection, it is no longer possible to make a fully automatic login. One can however reuse authorization token after a manual login. The token can then be refreshed automatically, for example with a cronjob (at least once per hour.)
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
pytolino
|
|
29
37
|
===================
|
|
30
38
|
|
|
31
|
-
A client to interact (login, upload, delete ebooks, etc..) with the tolino cloud with python.
|
|
39
|
+
A client to interact (login, upload, delete ebooks, etc..) with the tolino cloud with python. thanks to https://github.com/darkphoenix/tolino-calibre-sync for the inspiration.
|
|
32
40
|
|
|
33
41
|
One difference is that I aim to create a python package from it and to put it on pypi, so that one can use this python module in other projects.
|
|
34
42
|
|
|
@@ -42,48 +50,50 @@ Installation
|
|
|
42
50
|
Usage
|
|
43
51
|
=====
|
|
44
52
|
|
|
53
|
+
First, login manually, and use an inspector tool in the browser to inspect the requests. After connecting to the digital libray of tolino, there is POST request (named token). From the request response, copy the value of the refresh token (and the expiration time in seconds). Then, in a PATH request, in the request header, find the device_id number.
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
You can then store the token:
|
|
47
56
|
|
|
48
57
|
.. code-block:: python
|
|
49
58
|
|
|
50
|
-
from pytolino.tolino_cloud import Client
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
client
|
|
54
|
-
|
|
59
|
+
from pytolino.tolino_cloud import Client
|
|
60
|
+
partner = 'orellfuessli'
|
|
61
|
+
account_name = 'any name for reference'
|
|
62
|
+
client = Client(partner)
|
|
63
|
+
print('login on your browser and get the token.')
|
|
64
|
+
refresh_token = input('refresh token:\n')
|
|
65
|
+
expires_in = int(input('expires_in:\n'))
|
|
66
|
+
hardware_id = input('hardware id:\n')
|
|
67
|
+
Client.store_token(
|
|
68
|
+
account_name, refresh_token, expires_in, hardware_id)
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
Then, get a new access token. It will expires in 1 hours, so you might want to create a crontab job to do it regularely:
|
|
57
71
|
|
|
58
72
|
.. code-block:: python
|
|
59
73
|
|
|
60
|
-
from pytolino.tolino_cloud import Client
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
from pytolino.tolino_cloud import Client
|
|
75
|
+
partner = 'orellfuessli'
|
|
76
|
+
account_name = 'any name for reference'
|
|
77
|
+
client = Client(partner)
|
|
78
|
+
client.get_new_token(account_name)
|
|
79
|
+
|
|
80
|
+
After this, instead of login, you only need to retrieve the access token that is stored on disk and upload, delete books, etc...
|
|
81
|
+
|
|
82
|
+
.. code-block:: python
|
|
83
|
+
|
|
84
|
+
from pytolino.tolino_cloud import Client
|
|
85
|
+
partner = 'orellfuessli'
|
|
86
|
+
account_name = 'any name for reference'
|
|
87
|
+
client = Client(partner)
|
|
88
|
+
client.retrieve_token(account_name)
|
|
89
|
+
|
|
63
90
|
ebook_id = client.upload(EPUB_FILE_PATH) # return a unique id that can be used for reference
|
|
64
91
|
client.add_collection(epub_id, 'science fiction') # add the previous book to the collection science-fiction
|
|
65
92
|
client.add_cover(epub_id, cover_path) # to upload a cover on the book.
|
|
66
93
|
client.delete_ebook(epub_id) # delete the previousely uploaded ebook
|
|
67
94
|
inventory = client.get_inventory() # get a list of all the books on the cloud and their metadata
|
|
68
95
|
client.upload_metadata(epub_id, title='my title', author='someone') # you can upload various kind of metadata
|
|
69
|
-
client.logout()
|
|
70
|
-
|
|
71
96
|
|
|
72
|
-
if you want to unregister your computer:
|
|
73
|
-
|
|
74
|
-
.. code-block:: python
|
|
75
|
-
|
|
76
|
-
from pytolino.tolino_cloud import Client, PytolinoException
|
|
77
|
-
client = Client()
|
|
78
|
-
client.login(USERNAME, PASSWORD)
|
|
79
|
-
client.register() # now you will not be able to upload books from this computer
|
|
80
|
-
client.logout()
|
|
81
|
-
|
|
82
|
-
By default, it will connect to 'www.buecher.de'. In principle you could change the partner with:
|
|
83
|
-
|
|
84
|
-
.. code-block:: python
|
|
85
|
-
|
|
86
|
-
client = Client(server_name='www.orelfuessli') # for example if you have an account at orel fuessli.
|
|
87
97
|
|
|
88
98
|
To get a list of the supported partners:
|
|
89
99
|
|
|
@@ -92,22 +102,17 @@ To get a list of the supported partners:
|
|
|
92
102
|
from pytolino.tolino_cloud import PARTNERS
|
|
93
103
|
print(PARTNERS)
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
for now, only orelfuessli is supported, but it should be easy to include the others (but always need of a manual login)
|
|
97
106
|
|
|
98
107
|
|
|
99
108
|
Features
|
|
100
109
|
========
|
|
101
110
|
|
|
102
|
-
* login to tolino partner (for now works only with buecher.de)
|
|
103
|
-
* register device
|
|
104
|
-
* unregister device
|
|
105
111
|
* upload ebook
|
|
106
112
|
* delete ebook from the cloud
|
|
107
113
|
* add a book to a collection
|
|
108
114
|
* download inventory
|
|
109
115
|
* upload metadata
|
|
110
|
-
* more to come...
|
|
111
116
|
|
|
112
117
|
|
|
113
118
|
License
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pytolino/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pytolino/tolino_cloud.py,sha256=siPo3jsvtya2zCqsrQhc8ca9wyCC3SaCp0JM2Cw8IFI,13616
|
|
3
|
+
pytolino-2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
4
|
+
pytolino-2.0.dist-info/METADATA,sha256=jsrSK9m6lTbYoXNlgnIftWndbA3rXzdqeN9N4BbUXvI,4265
|
|
5
|
+
pytolino-2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
pytolino-2.0.dist-info/top_level.txt,sha256=3stGCqihEMMqlWGkME45OTJ0Prg-FO_kl554rtYNeuU,9
|
|
7
|
+
pytolino-2.0.dist-info/RECORD,,
|
pytolino/servers_settings.ini
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# buecher.de
|
|
2
|
-
[www.buecher.de]
|
|
3
|
-
partner_id = 30
|
|
4
|
-
client_id = dte_ereader_app_01
|
|
5
|
-
scope = ebook_library
|
|
6
|
-
signup_url = https://www.buecher.de/go/my_dry/my_register_aos/
|
|
7
|
-
profile_url = https://www.buecher.de/go/my_dry/my_login/receiver_object/my_login/
|
|
8
|
-
token_url = https://www.buecher.de/oauth2/token
|
|
9
|
-
revoke_url = https://www.buecher.de/oauth2/revoke
|
|
10
|
-
auth_url = https://www.buecher.de/oauth2/authorize
|
|
11
|
-
login_url = https://www.buecher.de/go/my_dry/my_login/
|
|
12
|
-
username_field = form[login]
|
|
13
|
-
password_field = form[password]
|
|
14
|
-
form_id = login
|
|
15
|
-
x_buecherde.skin_id = de_dte_tolino
|
|
16
|
-
login_cookie = session
|
|
17
|
-
logout_url = https://www.buecher.de/go/my_dry/my_logout/
|
|
18
|
-
reader_url = https://webreader.mytolino.com/library/
|
|
19
|
-
register_url = https://bosh.pageplace.de/bosh/rest/v2/registerhw
|
|
20
|
-
devices_url = https://bosh.pageplace.de/bosh/rest/handshake/devices/list
|
|
21
|
-
unregister_url = https://bosh.pageplace.de/bosh/rest/handshake/devices/delete
|
|
22
|
-
upload_url = https://bosh.pageplace.de/bosh/rest/upload
|
|
23
|
-
meta_url = https://bosh.pageplace.de/bosh/rest/meta
|
|
24
|
-
cover_url = https://bosh.pageplace.de/bosh/rest/cover
|
|
25
|
-
sync_data_url = https://bosh.pageplace.de/bosh/rest/sync-data?paths=publications,audiobooks
|
|
26
|
-
delete_url = https://bosh.pageplace.de/bosh/rest/deletecontent
|
|
27
|
-
inventory_url = https://bosh.pageplace.de/bosh/rest/inventory/delta
|
|
28
|
-
downloadinfo_url = https://bosh.pageplace.de/bosh/rest//cloud/downloadinfo/{}/{}/type/external-download
|
|
29
|
-
form_send = 1
|
pytolino-1.6.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pytolino/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
pytolino/servers_settings.ini,sha256=IDhstCWbLHaMj3qEOYcbDABaIUKHIz1jEBke3uSE8bI,1473
|
|
3
|
-
pytolino/tolino_cloud.py,sha256=HwrEil5FT-L-4jebc4mC9ThdlgEUGIGc719wqbJpBdY,19239
|
|
4
|
-
pytolino-1.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
-
pytolino-1.6.dist-info/METADATA,sha256=FkfXV1lncQyg66TrLTI_l_rGLQ2VjBq6CCuB-MUMvks,3725
|
|
6
|
-
pytolino-1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
pytolino-1.6.dist-info/top_level.txt,sha256=3stGCqihEMMqlWGkME45OTJ0Prg-FO_kl554rtYNeuU,9
|
|
8
|
-
pytolino-1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|