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 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
- from requests.adapters import HTTPAdapter
16
- import mechanize
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
- SERVERS_SETTINGS_FN = 'servers_settings.ini'
24
- SERVERS_SETTINGS_FILE_PATH = os.path.join(
25
- os.path.dirname(__file__),
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
- PARTNERS = servers_settings.sections()
32
- TOTAL_RETRY = 5
33
- STATUS_FORCELIST = [404]
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
- print(SERVERS_SETTINGS_FILE_PATH)
38
- print(servers_settings.sections())
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 _log_requests(self, host_response, error: True or None=None):
46
- if host_response.status_code >= 400 or error is True:
47
- logger = logging.error
48
- else:
49
- logger = logging.debug
50
- logger('log request')
51
- logger('---------------- HTTP response (requests)----------')
52
- logger(f'status code: {host_response.status_code}')
53
- logger(f'cookies: {host_response.cookies}')
54
- logger(f'headers: {host_response.headers}')
55
- try:
56
- j = host_response.json()
57
- logger(f'json: {j}')
58
- except requests.JSONDecodeError:
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
- try:
63
- host_response.raise_for_status()
64
- except requests.exceptions.HTTPError as e:
65
- raise PytolinoException
66
- except requests.exceptions.RequestException as e:
67
- raise PytolinoException
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
- hardware_id = _hardware_id()
64
+ @property
65
+ def refresh_token(self) -> str:
66
+ """refresh token to get new access token"""
67
+ return self._refresh_token
136
68
 
137
- def __init__(self, server_name='www.buecher.de'):
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.access_token = None
145
- self.refresh_token = None
146
- self.token_expires = None
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
- :username: str
166
- :password: str
167
- :returns: None, but raises pytolino exceptions if fail
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
- self.browser.open(self.server_settings['login_url'])
172
- self.browser.select_form(id=self.server_settings['form_id'])
173
- self.browser[self.server_settings['username_field']] = username
174
- self.browser[self.server_settings['password_field']] = password
175
- host_response = self.browser.submit()
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
- for cookie in self.browser.cookiejar:
178
- self.session.cookies.set(cookie.name, cookie.value)
98
+ :account_name: internal name under which token was stored
99
+ :returns: refresh_token, hardware_id
179
100
 
180
- logging.debug(self.server_settings['login_cookie'])
181
- self._log_mechanize(host_response)
182
- if not self.server_settings['login_cookie'] in self.session.cookies:
183
- raise PytolinoException(f'login to {self.server_name} failed.')
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
- auth_code = ""
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
- params = {
188
- 'client_id': self.server_settings['client_id'],
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
- self._log_requests(host_response)
121
+ """
122
+ self.retrieve_token(account_name)
206
123
 
207
- try:
208
- params = parse_qs(urlparse(
209
- host_response.headers['Location']
210
- ).query)
211
- auth_code = params['code'][0]
212
- except KeyError:
213
- self._log_requests(host_response, error=True)
214
- raise PytolinoException('oauth code request failed.')
215
-
216
- # Fetch OAUTH access token
217
- host_response = self.session.post(
218
- self.server_settings['token_url'],
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=False,
137
+ allow_redirects=True,
138
+ headers=headers,
139
+ impersonate='chrome',
228
140
  )
229
- self._log_requests(host_response)
230
-
231
- try:
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.access_token = j['access_token']
234
- self.refresh_token = j['refresh_token']
235
- self.token_expires = int(j['expires_in'])
236
- except requests.JSONDecodeError:
237
- raise PytolinoException('oauth access token request failed.')
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
- if 'revoke_url' in self.server_settings:
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
- """register your device. Needs to done only once! necessary to
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
- """unregister a device from the host partner. If no device is given,
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.session.get(
335
- self.server_settings['inventory_url'],
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.access_token,
339
- 'hardware_id': self.hardware_id,
340
- 'reseller_id': self.server_settings['partner_id'],
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
- self._log_requests(host_response)
345
- if host_response.status_code != 200:
346
- raise PytolinoException('invetory request failed')
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.session.patch(
390
- self.server_settings['sync_data_url'],
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.access_token,
247
+ 't_auth_token': self._access_token,
395
248
  'hardware_id': self.hardware_id,
396
- 'reseller_id': self.server_settings['partner_id'],
249
+ 'reseller_id': self._server_settings['partner_id'],
397
250
  'client_type': 'TOLINO_WEBREADER',
398
251
  }
399
252
  )
400
- self._log_requests(host_response)
401
- if host_response.status_code != 200:
402
- raise PytolinoException('add to collection failed')
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.server_settings['meta_url'] + f'/?deliverableId={book_id}'
413
- host_response = self.session.get(
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.access_token,
270
+ 't_auth_token': self._access_token,
417
271
  'hardware_id': self.hardware_id,
418
- 'reseller_id': self.server_settings['partner_id'],
272
+ 'reseller_id': self._server_settings['partner_id'],
419
273
  }
420
274
  )
421
275
 
422
276
  book = host_response.json()
423
- self._log_requests(host_response)
424
- if host_response.status_code != 200:
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.session.put(
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.access_token,
292
+ 't_auth_token': self._access_token,
440
293
  'hardware_id': self.hardware_id,
441
- 'reseller_id': self.server_settings['partner_id'],
294
+ 'reseller_id': self._server_settings['partner_id'],
442
295
  }
443
296
  )
444
297
 
445
- self._log_requests(host_response)
446
- if host_response.status_code != 200:
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.session.post(
470
- self.server_settings['upload_url'],
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.access_token,
332
+ 't_auth_token': self._access_token,
481
333
  'hardware_id': self.hardware_id,
482
- 'reseller_id': self.server_settings['partner_id'],
334
+ 'reseller_id': self._server_settings['partner_id'],
483
335
  }
484
336
  )
485
- self._log_requests(host_response)
486
- if host_response.status_code != 200:
487
- raise PytolinoException('file upload failed.')
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.session.get(
506
- self.server_settings['delete_url'],
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.access_token,
362
+ 't_auth_token': self._access_token,
510
363
  'hardware_id': self.hardware_id,
511
- 'reseller_id': self.server_settings['partner_id'],
364
+ 'reseller_id': self._server_settings['partner_id'],
512
365
  }
513
366
  )
514
- self._log_requests(host_response)
515
367
 
516
- if host_response.status_code != 200:
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.session.post(
547
- self.server_settings['cover_url'],
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.access_token,
403
+ 't_auth_token': self._access_token,
552
404
  'hardware_id': self.hardware_id,
553
- 'reseller_id': self.server_settings['partner_id'],
405
+ 'reseller_id': self._server_settings['partner_id'],
554
406
  },
555
407
  )
556
408
 
557
- self._log_requests(host_response)
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: 1.7
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. Most of the code is forked from https://github.com/darkphoenix/tolino-calibre-sync
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
- Before being able to send requests, you need to register your computer on which you will run the code:
55
+ You can then store the token:
47
56
 
48
57
  .. code-block:: python
49
58
 
50
- from pytolino.tolino_cloud import Client, PytolinoException
51
- client = Client()
52
- client.login(USERNAME, PASSWORD)
53
- client.register() # do this only once!
54
- client.logout()
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
- You can then upload, add to a collection or delete ebook on your cloud:
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, PytolinoException
61
- client = Client()
62
- client.login(USERNAME, PASSWORD)
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
- Unfortunately, the only supported partner now is 'www.buecher.de', because it has a different way of connection... So for now, the only solution is to create an account there and link it to your original account.
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
@@ -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,,