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 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
- 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):
46
- logging.info('-------------------- HTTP response --------------------')
47
- logging.info(f'status code: {host_response.status_code}')
48
- logging.info(f'cookies: {host_response.cookies}')
49
- logging.info(f'headers: {host_response.headers}')
50
- try:
51
- j = host_response.json()
52
- logging.debug(f'json: {j}')
53
- except requests.JSONDecodeError:
54
- logging.debug(f'text: {host_response.text}')
55
- logging.info('-------------------------------------------------------')
56
-
57
- def _log_mechanize(self, host_response):
58
- logging.info('-------------------- HTTP response --------------------')
59
- logging.info(f'status code: {host_response.code}')
60
- logging.info(f'headers: {host_response.info()}')
61
- logging.info('-------------------------------------------------------')
62
-
63
- def _hardware_id():
64
-
65
- # tolino wants to know a few details about the HTTP client hardware
66
- # when it connects.
67
- #
68
- # 1233X-44XXX-XXXXX-XXXXX-XXXXh
69
- #
70
- # 1 = os id
71
- # 2 = browser engine id
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
- hardware_id = _hardware_id()
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='www.buecher.de'):
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.access_token = None
127
- self.refresh_token = None
128
- self.token_expires = None
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
- :username: str
148
- :password: str
149
- :returns: None, but raises pytolino exceptions if fail
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
- self.browser.open(self.server_settings['login_url'])
154
- self.browser.select_form(id=self.server_settings['form_id'])
155
- self.browser[self.server_settings['username_field']] = username
156
- self.browser[self.server_settings['password_field']] = password
157
- 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
158
97
 
159
- for cookie in self.browser.cookiejar:
160
- self.session.cookies.set(cookie.name, cookie.value)
98
+ :account_name: internal name under which token was stored
99
+ :returns: refresh_token, hardware_id
161
100
 
162
- logging.info(self.server_settings['login_cookie'])
163
- self._log_mechanize(host_response)
164
- if not self.server_settings['login_cookie'] in self.session.cookies:
165
- 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
166
114
 
167
- auth_code = ""
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
- params = {
170
- 'client_id': self.server_settings['client_id'],
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
- self._log_requests(host_response)
121
+ """
122
+ self.retrieve_token(account_name)
188
123
 
189
- try:
190
- params = parse_qs(urlparse(
191
- host_response.headers['Location']
192
- ).query)
193
- auth_code = params['code'][0]
194
- except ValueError:
195
- raise PytolinoException('oauth code request failed.')
196
-
197
- # Fetch OAUTH access token
198
- host_response = self.session.post(
199
- self.server_settings['token_url'],
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=False,
137
+ allow_redirects=True,
138
+ headers=headers,
139
+ impersonate='chrome',
209
140
  )
210
- self._log_requests(host_response)
211
-
212
- 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:
213
147
  j = host_response.json()
214
- self.access_token = j['access_token']
215
- self.refresh_token = j['refresh_token']
216
- self.token_expires = int(j['expires_in'])
217
- except requests.JSONDecodeError:
218
- 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)
219
168
 
220
169
  def logout(self):
221
170
  """logout from tolino partner host
222
171
 
223
172
  """
224
- if 'revoke_url' in self.server_settings:
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
- """register your device. Needs to done only once! necessary to
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
- """unregister a device from the host partner. If no device is given,
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.session.get(
316
- self.server_settings['inventory_url'],
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.access_token,
320
- 'hardware_id': self.hardware_id,
321
- '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'],
322
194
  }
323
195
  )
324
196
 
325
- self._log_requests(host_response)
326
- if host_response.status_code != 200:
327
- raise PytolinoException('invetory request failed')
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.session.patch(
371
- self.server_settings['sync_data_url'],
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.access_token,
247
+ 't_auth_token': self._access_token,
376
248
  'hardware_id': self.hardware_id,
377
- 'reseller_id': self.server_settings['partner_id'],
249
+ 'reseller_id': self._server_settings['partner_id'],
378
250
  'client_type': 'TOLINO_WEBREADER',
379
251
  }
380
252
  )
381
- self._log_requests(host_response)
382
- if host_response.status_code != 200:
383
- raise PytolinoException('add to collection failed')
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.server_settings['meta_url'] + f'/?deliverableId={book_id}'
394
- host_response = self.session.get(
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.access_token,
270
+ 't_auth_token': self._access_token,
398
271
  'hardware_id': self.hardware_id,
399
- 'reseller_id': self.server_settings['partner_id'],
272
+ 'reseller_id': self._server_settings['partner_id'],
400
273
  }
401
274
  )
402
275
 
403
276
  book = host_response.json()
404
- self._log_requests(host_response)
405
- if host_response.status_code != 200:
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.session.put(
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.access_token,
292
+ 't_auth_token': self._access_token,
421
293
  'hardware_id': self.hardware_id,
422
- 'reseller_id': self.server_settings['partner_id'],
294
+ 'reseller_id': self._server_settings['partner_id'],
423
295
  }
424
296
  )
425
297
 
426
- self._log_requests(host_response)
427
- if host_response.status_code != 200:
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.session.post(
451
- self.server_settings['upload_url'],
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.access_token,
332
+ 't_auth_token': self._access_token,
462
333
  'hardware_id': self.hardware_id,
463
- 'reseller_id': self.server_settings['partner_id'],
334
+ 'reseller_id': self._server_settings['partner_id'],
464
335
  }
465
336
  )
466
- self._log_requests(host_response)
467
- if host_response.status_code != 200:
468
- raise PytolinoException('file upload failed.')
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.session.get(
487
- self.server_settings['delete_url'],
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.access_token,
362
+ 't_auth_token': self._access_token,
491
363
  'hardware_id': self.hardware_id,
492
- 'reseller_id': self.server_settings['partner_id'],
364
+ 'reseller_id': self._server_settings['partner_id'],
493
365
  }
494
366
  )
495
- self._log_requests(host_response)
496
367
 
497
- if host_response.status_code != 200:
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 filepath has no extension
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.session.post(
527
- self.server_settings['cover_url'],
528
- files = [('file', ('1092560016', open(filepath, 'rb'), mime))],
529
- data = {'deliverableId': book_id},
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.access_token,
403
+ 't_auth_token': self._access_token,
532
404
  'hardware_id': self.hardware_id,
533
- 'reseller_id': self.server_settings['partner_id'],
405
+ 'reseller_id': self._server_settings['partner_id'],
534
406
  },
535
407
  )
536
408
 
537
- self._log_requests(host_response)
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: 1.6
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=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,,