pytolino 1.7__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 +171 -321
- {pytolino-1.7.dist-info → pytolino-2.0.dist-info}/METADATA +40 -35
- pytolino-2.0.dist-info/RECORD +7 -0
- {pytolino-1.7.dist-info → pytolino-2.0.dist-info}/WHEEL +1 -1
- pytolino/servers_settings.ini +0 -29
- pytolino-1.7.dist-info/RECORD +0 -8
- {pytolino-1.7.dist-info → pytolino-2.0.dist-info}/licenses/LICENSE +0 -0
- {pytolino-1.7.dist-info → pytolino-2.0.dist-info}/top_level.txt +0 -0
pytolino/tolino_cloud.py
CHANGED
|
@@ -1,329 +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
|
-
logger(f'text: {host_response.text}')
|
|
60
|
-
logger('-------------------------------------------------------')
|
|
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.
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _log_mechanize(self, host_response):
|
|
70
|
-
logging.debug('-------------- HTTP response (mechanize)--------------')
|
|
71
|
-
logging.debug(f'status code: {host_response.code}')
|
|
72
|
-
logging.debug(f'headers: {host_response.info()}')
|
|
73
|
-
logging.debug('-------------------------------------------------------')
|
|
74
|
-
if host_response.code >= 400:
|
|
75
|
-
logging.error('-------------- HTTP response (mechanize)----------')
|
|
76
|
-
logging.error(f'status code: {host_response.code}')
|
|
77
|
-
logging.error(f'headers: {host_response.info()}')
|
|
78
|
-
logging.error('--------------------------------------------------')
|
|
79
|
-
raise PytolinoException('http error')
|
|
80
|
-
|
|
81
|
-
def _hardware_id():
|
|
82
|
-
|
|
83
|
-
# tolino wants to know a few details about the HTTP client hardware
|
|
84
|
-
# when it connects.
|
|
85
|
-
#
|
|
86
|
-
# 1233X-44XXX-XXXXX-XXXXX-XXXXh
|
|
87
|
-
#
|
|
88
|
-
# 1 = os id
|
|
89
|
-
# 2 = browser engine id
|
|
90
|
-
# 33 = browser id
|
|
91
|
-
# 44 = browser version
|
|
92
|
-
# X = the result of a fingerprinting image
|
|
93
|
-
|
|
94
|
-
os_id = {
|
|
95
|
-
'Windows': '1',
|
|
96
|
-
'Darwin': '2',
|
|
97
|
-
'Linux': '3'
|
|
98
|
-
}.get(platform.system(), 'x')
|
|
99
|
-
|
|
100
|
-
# The hardware id contains some info about the browser
|
|
101
|
-
#
|
|
102
|
-
# Hey, tolino developers: Let me know which id values to use here
|
|
103
|
-
engine_id = 'x'
|
|
104
|
-
browser_id = 'xx'
|
|
105
|
-
version_id = '00'
|
|
106
|
-
|
|
107
|
-
# For some odd reason, the tolino javascript draws the text
|
|
108
|
-
# "www.tolino.de" and a rectangle filled with the offical Telekom
|
|
109
|
-
# magenta #E20074 (http://de.wikipedia.org/wiki/Magenta_%28Farbe%29)
|
|
110
|
-
# into an image canvas and then fuddles around with the
|
|
111
|
-
# base64-encoded PNG. Probably to gain some sort of fingerprint,
|
|
112
|
-
# but it's not quite clear how this would help the tolino API.
|
|
113
|
-
#
|
|
114
|
-
# Hey, tolino developers: Let me know what you need here.
|
|
115
|
-
|
|
116
|
-
fingerprint = 'ABCDEFGHIJKLMNOPQR'
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
os_id +
|
|
120
|
-
engine_id +
|
|
121
|
-
browser_id +
|
|
122
|
-
fingerprint[0:1] +
|
|
123
|
-
'-' +
|
|
124
|
-
version_id +
|
|
125
|
-
fingerprint[1:4] +
|
|
126
|
-
'-' +
|
|
127
|
-
fingerprint[4:9] +
|
|
128
|
-
'-' +
|
|
129
|
-
fingerprint[9:14] +
|
|
130
|
-
'-' +
|
|
131
|
-
fingerprint[14:18] +
|
|
132
|
-
'h'
|
|
133
|
-
)
|
|
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
|
|
134
63
|
|
|
135
|
-
|
|
64
|
+
@property
|
|
65
|
+
def refresh_token(self) -> str:
|
|
66
|
+
"""refresh token to get new access token"""
|
|
67
|
+
return self._refresh_token
|
|
136
68
|
|
|
137
|
-
|
|
69
|
+
@property
|
|
70
|
+
def hardware_id(self) -> str:
|
|
71
|
+
"""hardware id that is sent in request payloads"""
|
|
72
|
+
return self._hardware_id
|
|
73
|
+
|
|
74
|
+
def __init__(self, server_name='orellfuessli'):
|
|
138
75
|
|
|
139
76
|
if server_name not in servers_settings:
|
|
140
77
|
raise PytolinoException(
|
|
141
78
|
f'the partner {server_name} was not found.'
|
|
142
79
|
f'please choose one of the list: {PARTNERS}')
|
|
143
80
|
|
|
144
|
-
self.
|
|
145
|
-
self.
|
|
146
|
-
self.
|
|
147
|
-
|
|
148
|
-
self.server_settings = servers_settings[server_name]
|
|
149
|
-
self.session = requests.Session()
|
|
150
|
-
retry_strategy = Retry(
|
|
151
|
-
total=TOTAL_RETRY,
|
|
152
|
-
status_forcelist=STATUS_FORCELIST,
|
|
153
|
-
backoff_factor=2,
|
|
154
|
-
allowed_methods=frozenset(['GET', 'POST']))
|
|
155
|
-
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
156
|
-
self.session.mount('http://', adapter)
|
|
157
|
-
self.session.mount('https://', adapter)
|
|
158
|
-
self.browser = mechanize.Browser()
|
|
159
|
-
self.browser.set_handle_robots(False)
|
|
160
|
-
self.server_name = server_name
|
|
161
|
-
|
|
162
|
-
def login(self, username, password):
|
|
163
|
-
"""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
|
|
164
85
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
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
|
|
170
90
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
97
|
|
|
177
|
-
|
|
178
|
-
|
|
98
|
+
:account_name: internal name under which token was stored
|
|
99
|
+
:returns: refresh_token, hardware_id
|
|
179
100
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if not
|
|
183
|
-
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
|
|
184
114
|
|
|
185
|
-
|
|
115
|
+
def get_new_token(self, account_name):
|
|
116
|
+
"""look at the store token, and get a new access and refresh tokens.
|
|
186
117
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
'response_type': 'code',
|
|
190
|
-
'scope': self.server_settings['scope'],
|
|
191
|
-
'redirect_uri': self.server_settings['reader_url']
|
|
192
|
-
}
|
|
193
|
-
if 'login_form_url' in self.server_settings:
|
|
194
|
-
params['x_buchde.skin_id'] = self.server_settings[
|
|
195
|
-
'x_buchde.skin_id']
|
|
196
|
-
params['x_buchde.mandant_id'] = self.server_settings[
|
|
197
|
-
'x_buchde.mandant_id']
|
|
198
|
-
host_response = self.session.get(
|
|
199
|
-
self.server_settings['auth_url'],
|
|
200
|
-
params=params,
|
|
201
|
-
verify=True,
|
|
202
|
-
allow_redirects=False,
|
|
203
|
-
)
|
|
118
|
+
:account_name: TODO
|
|
119
|
+
:returns: TODO
|
|
204
120
|
|
|
205
|
-
|
|
121
|
+
"""
|
|
122
|
+
self.retrieve_token(account_name)
|
|
206
123
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
data={
|
|
220
|
-
'client_id': self.server_settings['client_id'],
|
|
221
|
-
'grant_type': 'authorization_code',
|
|
222
|
-
'code': auth_code,
|
|
223
|
-
'scope': self.server_settings['scope'],
|
|
224
|
-
'redirect_uri': self.server_settings['reader_url']
|
|
225
|
-
},
|
|
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,
|
|
226
136
|
verify=True,
|
|
227
|
-
allow_redirects=
|
|
137
|
+
allow_redirects=True,
|
|
138
|
+
headers=headers,
|
|
139
|
+
impersonate='chrome',
|
|
228
140
|
)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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:
|
|
232
147
|
j = host_response.json()
|
|
233
|
-
self.
|
|
234
|
-
self.
|
|
235
|
-
self.
|
|
236
|
-
|
|
237
|
-
|
|
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)
|
|
238
168
|
|
|
239
169
|
def logout(self):
|
|
240
170
|
"""logout from tolino partner host
|
|
241
171
|
|
|
242
172
|
"""
|
|
243
|
-
|
|
244
|
-
host_response = self.session.post(
|
|
245
|
-
self.server_settings['revoke_url'],
|
|
246
|
-
data={
|
|
247
|
-
'client_id': self.server_settings['client_id'],
|
|
248
|
-
'token_type': 'refresh_token',
|
|
249
|
-
'token': self.refresh_token,
|
|
250
|
-
}
|
|
251
|
-
)
|
|
252
|
-
self._log_requests(host_response)
|
|
253
|
-
if host_response.status_code != 200:
|
|
254
|
-
raise PytolinoException('logout failed.')
|
|
255
|
-
else:
|
|
256
|
-
host_response = self.session.post(
|
|
257
|
-
self.server_settings['logout_url'],
|
|
258
|
-
)
|
|
259
|
-
self._log_requests(host_response)
|
|
260
|
-
if host_response.status_code != 200:
|
|
261
|
-
raise PytolinoException('logout failed.')
|
|
173
|
+
raise NotImplementedError('logout is not necessary with tokens')
|
|
262
174
|
|
|
263
175
|
def register(self):
|
|
264
|
-
|
|
265
|
-
upload files. you need to login first.
|
|
266
|
-
|
|
267
|
-
"""
|
|
268
|
-
host_response = self.session.post(
|
|
269
|
-
self.server_settings['register_url'],
|
|
270
|
-
data=json.dumps({'hardware_name': 'tolino sync reader'}),
|
|
271
|
-
headers={
|
|
272
|
-
'content-type': 'application/json',
|
|
273
|
-
't_auth_token': self.access_token,
|
|
274
|
-
'hardware_id': self.hardware_id,
|
|
275
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
276
|
-
'client_type': 'TOLINO_WEBREADER',
|
|
277
|
-
'client_version': '4.4.1',
|
|
278
|
-
'hardware_type': 'HTML5',
|
|
279
|
-
}
|
|
280
|
-
)
|
|
281
|
-
self._log_requests(host_response)
|
|
282
|
-
if host_response.status_code != 200:
|
|
283
|
-
raise PytolinoException(f'register {self.hardware_id} failed.')
|
|
176
|
+
raise NotImplementedError('register is not necessary with tokens')
|
|
284
177
|
|
|
285
178
|
def unregister(self, device_id=None):
|
|
286
|
-
|
|
287
|
-
it is assumed the device in use will be removed
|
|
288
|
-
|
|
289
|
-
:device_id: None or str if we want to unregister another device
|
|
290
|
-
:returns: None
|
|
291
|
-
|
|
292
|
-
"""
|
|
293
|
-
if device_id is None:
|
|
294
|
-
device_id = self.hardware_id
|
|
295
|
-
|
|
296
|
-
host_response = self.session.post(
|
|
297
|
-
self.server_settings['unregister_url'],
|
|
298
|
-
data=json.dumps({
|
|
299
|
-
'deleteDevicesRequest': {
|
|
300
|
-
'accounts': [{
|
|
301
|
-
'auth_token': self.access_token,
|
|
302
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
303
|
-
}],
|
|
304
|
-
'devices': [{
|
|
305
|
-
'device_id': device_id,
|
|
306
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
307
|
-
}]
|
|
308
|
-
}
|
|
309
|
-
}),
|
|
310
|
-
headers={
|
|
311
|
-
'content-type': 'application/json',
|
|
312
|
-
't_auth_token': self.access_token,
|
|
313
|
-
'reseller_id': self.server_settings['partner_id'],
|
|
314
|
-
}
|
|
315
|
-
)
|
|
316
|
-
self._log_requests(host_response)
|
|
317
|
-
if host_response.status_code != 200:
|
|
318
|
-
try:
|
|
319
|
-
j = host_response.json()
|
|
320
|
-
raise PytolinoException(
|
|
321
|
-
f"unregister {device_id} failed: ",
|
|
322
|
-
f"{j['ResponseInfo']['message']}"
|
|
323
|
-
)
|
|
324
|
-
except KeyError:
|
|
325
|
-
raise PytolinoException(
|
|
326
|
-
f'unregister {device_id} failed: reason unknown.')
|
|
179
|
+
raise NotImplementedError('unregister is not necessary with tokens')
|
|
327
180
|
|
|
328
181
|
def get_inventory(self):
|
|
329
182
|
"""download a list of the books on the cloud and their information
|
|
@@ -331,19 +184,19 @@ class Client(object):
|
|
|
331
184
|
|
|
332
185
|
"""
|
|
333
186
|
|
|
334
|
-
host_response = self.
|
|
335
|
-
self.
|
|
187
|
+
host_response = self._session.get(
|
|
188
|
+
self._server_settings['inventory_url'],
|
|
336
189
|
params={'strip': 'true'},
|
|
337
190
|
headers={
|
|
338
|
-
't_auth_token': self.
|
|
339
|
-
'hardware_id': self.
|
|
340
|
-
'reseller_id': self.
|
|
191
|
+
't_auth_token': self._access_token,
|
|
192
|
+
'hardware_id': self._hardware_id,
|
|
193
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
341
194
|
}
|
|
342
195
|
)
|
|
343
196
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
197
|
+
if not host_response.ok:
|
|
198
|
+
raise PytolinoException(
|
|
199
|
+
f'inventory request failed {host_response}')
|
|
347
200
|
|
|
348
201
|
try:
|
|
349
202
|
j = host_response.json()
|
|
@@ -386,20 +239,21 @@ class Client(object):
|
|
|
386
239
|
}]
|
|
387
240
|
}
|
|
388
241
|
|
|
389
|
-
host_response = self.
|
|
390
|
-
self.
|
|
242
|
+
host_response = self._session.patch(
|
|
243
|
+
self._server_settings['sync_data_url'],
|
|
391
244
|
data=json.dumps(payload),
|
|
392
245
|
headers={
|
|
393
246
|
'content-type': 'application/json',
|
|
394
|
-
't_auth_token': self.
|
|
247
|
+
't_auth_token': self._access_token,
|
|
395
248
|
'hardware_id': self.hardware_id,
|
|
396
|
-
'reseller_id': self.
|
|
249
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
397
250
|
'client_type': 'TOLINO_WEBREADER',
|
|
398
251
|
}
|
|
399
252
|
)
|
|
400
|
-
|
|
401
|
-
if host_response.
|
|
402
|
-
raise PytolinoException(
|
|
253
|
+
|
|
254
|
+
if not host_response.ok:
|
|
255
|
+
raise PytolinoException(
|
|
256
|
+
f'collection add failed {host_response}')
|
|
403
257
|
|
|
404
258
|
def upload_metadata(self, book_id, **new_metadata):
|
|
405
259
|
"""upload some metadata to a specific book on the cloud
|
|
@@ -409,20 +263,19 @@ class Client(object):
|
|
|
409
263
|
|
|
410
264
|
"""
|
|
411
265
|
|
|
412
|
-
url = self.
|
|
413
|
-
host_response = self.
|
|
266
|
+
url = self._server_settings['meta_url'] + f'/?deliverableId={book_id}'
|
|
267
|
+
host_response = self._session.get(
|
|
414
268
|
url,
|
|
415
269
|
headers={
|
|
416
|
-
't_auth_token': self.
|
|
270
|
+
't_auth_token': self._access_token,
|
|
417
271
|
'hardware_id': self.hardware_id,
|
|
418
|
-
'reseller_id': self.
|
|
272
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
419
273
|
}
|
|
420
274
|
)
|
|
421
275
|
|
|
422
276
|
book = host_response.json()
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
raise PytolinoException('metadata upload failed')
|
|
277
|
+
if not host_response.ok:
|
|
278
|
+
raise PytolinoException(f'metadata upload failed {host_response}')
|
|
426
279
|
|
|
427
280
|
for key, value in new_metadata.items():
|
|
428
281
|
book['metadata'][key] = value
|
|
@@ -431,20 +284,19 @@ class Client(object):
|
|
|
431
284
|
'uploadMetaData': book['metadata']
|
|
432
285
|
}
|
|
433
286
|
|
|
434
|
-
host_response = self.
|
|
287
|
+
host_response = self._session.put(
|
|
435
288
|
url,
|
|
436
289
|
data=json.dumps(payload),
|
|
437
290
|
headers={
|
|
438
291
|
'content-type': 'application/json',
|
|
439
|
-
't_auth_token': self.
|
|
292
|
+
't_auth_token': self._access_token,
|
|
440
293
|
'hardware_id': self.hardware_id,
|
|
441
|
-
'reseller_id': self.
|
|
294
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
442
295
|
}
|
|
443
296
|
)
|
|
444
297
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
raise PytolinoException('metadata upload failed')
|
|
298
|
+
if not host_response.ok:
|
|
299
|
+
raise PytolinoException(f'metadata upload failed {host_response}')
|
|
448
300
|
|
|
449
301
|
def upload(self, file_path, name=None, extension=None):
|
|
450
302
|
"""upload an ebook to your cloud
|
|
@@ -466,8 +318,8 @@ class Client(object):
|
|
|
466
318
|
'epub': 'application/epub+zip',
|
|
467
319
|
}.get(extension.lower(), 'application/pdf')
|
|
468
320
|
|
|
469
|
-
host_response = self.
|
|
470
|
-
self.
|
|
321
|
+
host_response = self._session.post(
|
|
322
|
+
self._server_settings['upload_url'],
|
|
471
323
|
files=[(
|
|
472
324
|
'file',
|
|
473
325
|
(
|
|
@@ -477,14 +329,15 @@ class Client(object):
|
|
|
477
329
|
),
|
|
478
330
|
)],
|
|
479
331
|
headers={
|
|
480
|
-
't_auth_token': self.
|
|
332
|
+
't_auth_token': self._access_token,
|
|
481
333
|
'hardware_id': self.hardware_id,
|
|
482
|
-
'reseller_id': self.
|
|
334
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
483
335
|
}
|
|
484
336
|
)
|
|
485
|
-
|
|
486
|
-
if host_response.
|
|
487
|
-
raise PytolinoException('
|
|
337
|
+
|
|
338
|
+
if not host_response.ok:
|
|
339
|
+
raise PytolinoException(f'upload failed {host_response}')
|
|
340
|
+
|
|
488
341
|
try:
|
|
489
342
|
j = host_response.json()
|
|
490
343
|
except requests.JSONDecodeError:
|
|
@@ -502,18 +355,17 @@ class Client(object):
|
|
|
502
355
|
:returns: None
|
|
503
356
|
|
|
504
357
|
"""
|
|
505
|
-
host_response = self.
|
|
506
|
-
self.
|
|
358
|
+
host_response = self._session.get(
|
|
359
|
+
self._server_settings['delete_url'],
|
|
507
360
|
params={'deliverableId': ebook_id},
|
|
508
361
|
headers={
|
|
509
|
-
't_auth_token': self.
|
|
362
|
+
't_auth_token': self._access_token,
|
|
510
363
|
'hardware_id': self.hardware_id,
|
|
511
|
-
'reseller_id': self.
|
|
364
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
512
365
|
}
|
|
513
366
|
)
|
|
514
|
-
self._log_requests(host_response)
|
|
515
367
|
|
|
516
|
-
if host_response.
|
|
368
|
+
if not host_response.ok:
|
|
517
369
|
try:
|
|
518
370
|
j = host_response.json()
|
|
519
371
|
raise PytolinoException(
|
|
@@ -543,21 +395,19 @@ class Client(object):
|
|
|
543
395
|
'jpg': 'image/jpeg'
|
|
544
396
|
}.get(ext.lower(), 'application/jpeg')
|
|
545
397
|
|
|
546
|
-
host_response = self.
|
|
547
|
-
self.
|
|
398
|
+
host_response = self._session.post(
|
|
399
|
+
self._server_settings['cover_url'],
|
|
548
400
|
files=[('file', ('1092560016', open(filepath, 'rb'), mime))],
|
|
549
401
|
data={'deliverableId': book_id},
|
|
550
402
|
headers={
|
|
551
|
-
't_auth_token': self.
|
|
403
|
+
't_auth_token': self._access_token,
|
|
552
404
|
'hardware_id': self.hardware_id,
|
|
553
|
-
'reseller_id': self.
|
|
405
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
554
406
|
},
|
|
555
407
|
)
|
|
556
408
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if host_response.status_code != 200:
|
|
560
|
-
raise PytolinoException('cover upload failed.')
|
|
409
|
+
if not host_response.ok:
|
|
410
|
+
raise PytolinoException(f'cover upload failed. {host_response}')
|
|
561
411
|
|
|
562
412
|
|
|
563
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.7.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=K3eeV2Zj6IgyoGEhCerTnawBJkOm8aUhz4ZrojQUayQ,20072
|
|
4
|
-
pytolino-1.7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
-
pytolino-1.7.dist-info/METADATA,sha256=UsWaJ9s46hClOr2k6rKSwNPjCUTmEJ6nF82JAvVC1r4,3725
|
|
6
|
-
pytolino-1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
pytolino-1.7.dist-info/top_level.txt,sha256=3stGCqihEMMqlWGkME45OTJ0Prg-FO_kl554rtYNeuU,9
|
|
8
|
-
pytolino-1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|