pytolino 1.6__tar.gz → 2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytolino-1.6/src/pytolino.egg-info → pytolino-2.0}/PKG-INFO +40 -35
- pytolino-2.0/README.rst +92 -0
- {pytolino-1.6 → pytolino-2.0}/pyproject.toml +4 -2
- pytolino-2.0/src/pytolino/tolino_cloud.py +414 -0
- {pytolino-1.6 → pytolino-2.0/src/pytolino.egg-info}/PKG-INFO +40 -35
- {pytolino-1.6 → pytolino-2.0}/src/pytolino.egg-info/SOURCES.txt +0 -1
- {pytolino-1.6 → pytolino-2.0}/src/pytolino.egg-info/requires.txt +2 -0
- pytolino-2.0/tests/test_tolino_cloud.py +170 -0
- pytolino-1.6/README.rst +0 -89
- pytolino-1.6/src/pytolino/servers_settings.ini +0 -29
- pytolino-1.6/src/pytolino/tolino_cloud.py +0 -544
- pytolino-1.6/tests/test_tolino_cloud.py +0 -249
- {pytolino-1.6 → pytolino-2.0}/LICENSE +0 -0
- {pytolino-1.6 → pytolino-2.0}/MANIFEST.in +0 -0
- {pytolino-1.6 → pytolino-2.0}/setup.cfg +0 -0
- {pytolino-1.6 → pytolino-2.0}/src/pytolino/__init__.py +0 -0
- {pytolino-1.6 → pytolino-2.0}/src/pytolino.egg-info/dependency_links.txt +0 -0
- {pytolino-1.6 → pytolino-2.0}/src/pytolino.egg-info/top_level.txt +0 -0
|
@@ -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
|
pytolino-2.0/README.rst
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
UPDATE
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
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.)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
pytolino
|
|
8
|
+
===================
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
Installation
|
|
15
|
+
============
|
|
16
|
+
|
|
17
|
+
.. code-block:: bash
|
|
18
|
+
|
|
19
|
+
pip install pytolino
|
|
20
|
+
|
|
21
|
+
Usage
|
|
22
|
+
=====
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
You can then store the token:
|
|
27
|
+
|
|
28
|
+
.. code-block:: python
|
|
29
|
+
|
|
30
|
+
from pytolino.tolino_cloud import Client
|
|
31
|
+
partner = 'orellfuessli'
|
|
32
|
+
account_name = 'any name for reference'
|
|
33
|
+
client = Client(partner)
|
|
34
|
+
print('login on your browser and get the token.')
|
|
35
|
+
refresh_token = input('refresh token:\n')
|
|
36
|
+
expires_in = int(input('expires_in:\n'))
|
|
37
|
+
hardware_id = input('hardware id:\n')
|
|
38
|
+
Client.store_token(
|
|
39
|
+
account_name, refresh_token, expires_in, hardware_id)
|
|
40
|
+
|
|
41
|
+
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:
|
|
42
|
+
|
|
43
|
+
.. code-block:: python
|
|
44
|
+
|
|
45
|
+
from pytolino.tolino_cloud import Client
|
|
46
|
+
partner = 'orellfuessli'
|
|
47
|
+
account_name = 'any name for reference'
|
|
48
|
+
client = Client(partner)
|
|
49
|
+
client.get_new_token(account_name)
|
|
50
|
+
|
|
51
|
+
After this, instead of login, you only need to retrieve the access token that is stored on disk and upload, delete books, etc...
|
|
52
|
+
|
|
53
|
+
.. code-block:: python
|
|
54
|
+
|
|
55
|
+
from pytolino.tolino_cloud import Client
|
|
56
|
+
partner = 'orellfuessli'
|
|
57
|
+
account_name = 'any name for reference'
|
|
58
|
+
client = Client(partner)
|
|
59
|
+
client.retrieve_token(account_name)
|
|
60
|
+
|
|
61
|
+
ebook_id = client.upload(EPUB_FILE_PATH) # return a unique id that can be used for reference
|
|
62
|
+
client.add_collection(epub_id, 'science fiction') # add the previous book to the collection science-fiction
|
|
63
|
+
client.add_cover(epub_id, cover_path) # to upload a cover on the book.
|
|
64
|
+
client.delete_ebook(epub_id) # delete the previousely uploaded ebook
|
|
65
|
+
inventory = client.get_inventory() # get a list of all the books on the cloud and their metadata
|
|
66
|
+
client.upload_metadata(epub_id, title='my title', author='someone') # you can upload various kind of metadata
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
To get a list of the supported partners:
|
|
70
|
+
|
|
71
|
+
.. code-block:: python
|
|
72
|
+
|
|
73
|
+
from pytolino.tolino_cloud import PARTNERS
|
|
74
|
+
print(PARTNERS)
|
|
75
|
+
|
|
76
|
+
for now, only orelfuessli is supported, but it should be easy to include the others (but always need of a manual login)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Features
|
|
80
|
+
========
|
|
81
|
+
|
|
82
|
+
* upload ebook
|
|
83
|
+
* delete ebook from the cloud
|
|
84
|
+
* add a book to a collection
|
|
85
|
+
* download inventory
|
|
86
|
+
* upload metadata
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
License
|
|
90
|
+
=======
|
|
91
|
+
|
|
92
|
+
The project is licensed under GNU GENERAL PUBLIC LICENSE v3.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytolino"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{name="Imam Usmani"},
|
|
10
10
|
]
|
|
@@ -21,6 +21,8 @@ description = "client for tolino cloud"
|
|
|
21
21
|
dependencies = [
|
|
22
22
|
'requests',
|
|
23
23
|
'mechanize',
|
|
24
|
+
'curl_cffi',
|
|
25
|
+
'varboxes',
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
[project.optional-dependencies]
|
|
@@ -31,7 +33,7 @@ dev = [
|
|
|
31
33
|
'sphinx',
|
|
32
34
|
'build',
|
|
33
35
|
'twine',
|
|
34
|
-
'sphinx-rtd-theme'
|
|
36
|
+
'sphinx-rtd-theme',
|
|
35
37
|
]
|
|
36
38
|
|
|
37
39
|
[project.urls]
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import curl_cffi
|
|
13
|
+
from varboxes import VarBox
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PytolinoException(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExpirationError(PytolinoException):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
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()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
for partner in PARTNERS:
|
|
32
|
+
print(partner)
|
|
33
|
+
for key, val in servers_settings[partner].items():
|
|
34
|
+
print(key, val)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Client(object):
|
|
38
|
+
|
|
39
|
+
"""create a client to communicate with a tolino partner (login, etc..)"""
|
|
40
|
+
|
|
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
|
|
68
|
+
|
|
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'):
|
|
75
|
+
|
|
76
|
+
if server_name not in servers_settings:
|
|
77
|
+
raise PytolinoException(
|
|
78
|
+
f'the partner {server_name} was not found.'
|
|
79
|
+
f'please choose one of the list: {PARTNERS}')
|
|
80
|
+
|
|
81
|
+
self._access_token = None
|
|
82
|
+
self._refresh_token = None
|
|
83
|
+
self._token_expires = None
|
|
84
|
+
self._hardware_id = None
|
|
85
|
+
|
|
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
|
|
90
|
+
|
|
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
|
|
97
|
+
|
|
98
|
+
:account_name: internal name under which token was stored
|
|
99
|
+
:returns: refresh_token, hardware_id
|
|
100
|
+
|
|
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
|
|
114
|
+
|
|
115
|
+
def get_new_token(self, account_name):
|
|
116
|
+
"""look at the store token, and get a new access and refresh tokens.
|
|
117
|
+
|
|
118
|
+
:account_name: TODO
|
|
119
|
+
:returns: TODO
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
self.retrieve_token(account_name)
|
|
123
|
+
|
|
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,
|
|
136
|
+
verify=True,
|
|
137
|
+
allow_redirects=True,
|
|
138
|
+
headers=headers,
|
|
139
|
+
impersonate='chrome',
|
|
140
|
+
)
|
|
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:
|
|
147
|
+
j = host_response.json()
|
|
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)
|
|
168
|
+
|
|
169
|
+
def logout(self):
|
|
170
|
+
"""logout from tolino partner host
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
raise NotImplementedError('logout is not necessary with tokens')
|
|
174
|
+
|
|
175
|
+
def register(self):
|
|
176
|
+
raise NotImplementedError('register is not necessary with tokens')
|
|
177
|
+
|
|
178
|
+
def unregister(self, device_id=None):
|
|
179
|
+
raise NotImplementedError('unregister is not necessary with tokens')
|
|
180
|
+
|
|
181
|
+
def get_inventory(self):
|
|
182
|
+
"""download a list of the books on the cloud and their information
|
|
183
|
+
:returns: list of dict describing the book, with a epubMetaData dict
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
host_response = self._session.get(
|
|
188
|
+
self._server_settings['inventory_url'],
|
|
189
|
+
params={'strip': 'true'},
|
|
190
|
+
headers={
|
|
191
|
+
't_auth_token': self._access_token,
|
|
192
|
+
'hardware_id': self._hardware_id,
|
|
193
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if not host_response.ok:
|
|
198
|
+
raise PytolinoException(
|
|
199
|
+
f'inventory request failed {host_response}')
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
j = host_response.json()
|
|
203
|
+
except requests.JSONDecodeError:
|
|
204
|
+
raise PytolinoException(
|
|
205
|
+
'inventory list request failed because of json error.'
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
try:
|
|
209
|
+
publication_inventory = j['PublicationInventory']
|
|
210
|
+
uploaded_ebooks = publication_inventory['edata']
|
|
211
|
+
purchased_ebook = publication_inventory['ebook']
|
|
212
|
+
except KeyError:
|
|
213
|
+
raise PytolinoException(
|
|
214
|
+
'inventory list request failed because',
|
|
215
|
+
'of key error in json.',
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
inventory = uploaded_ebooks + purchased_ebook
|
|
219
|
+
return inventory
|
|
220
|
+
|
|
221
|
+
def add_to_collection(self, book_id, collection_name):
|
|
222
|
+
"""add a book to a collection on the cloud
|
|
223
|
+
|
|
224
|
+
:book_id: identify the book on the cloud
|
|
225
|
+
:collection_name: str name
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
payload = {
|
|
230
|
+
"revision": None,
|
|
231
|
+
"patches": [{
|
|
232
|
+
"op": "add",
|
|
233
|
+
"value": {
|
|
234
|
+
"modified": round(time.time() * 1000),
|
|
235
|
+
"name": collection_name,
|
|
236
|
+
"category": "collection",
|
|
237
|
+
},
|
|
238
|
+
"path": f"/publications/{book_id}/tags"
|
|
239
|
+
}]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
host_response = self._session.patch(
|
|
243
|
+
self._server_settings['sync_data_url'],
|
|
244
|
+
data=json.dumps(payload),
|
|
245
|
+
headers={
|
|
246
|
+
'content-type': 'application/json',
|
|
247
|
+
't_auth_token': self._access_token,
|
|
248
|
+
'hardware_id': self.hardware_id,
|
|
249
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
250
|
+
'client_type': 'TOLINO_WEBREADER',
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if not host_response.ok:
|
|
255
|
+
raise PytolinoException(
|
|
256
|
+
f'collection add failed {host_response}')
|
|
257
|
+
|
|
258
|
+
def upload_metadata(self, book_id, **new_metadata):
|
|
259
|
+
"""upload some metadata to a specific book on the cloud
|
|
260
|
+
|
|
261
|
+
:book_id: ref on the cloud of the book
|
|
262
|
+
:**meta_data: dict of metadata than can be changed
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
url = self._server_settings['meta_url'] + f'/?deliverableId={book_id}'
|
|
267
|
+
host_response = self._session.get(
|
|
268
|
+
url,
|
|
269
|
+
headers={
|
|
270
|
+
't_auth_token': self._access_token,
|
|
271
|
+
'hardware_id': self.hardware_id,
|
|
272
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
book = host_response.json()
|
|
277
|
+
if not host_response.ok:
|
|
278
|
+
raise PytolinoException(f'metadata upload failed {host_response}')
|
|
279
|
+
|
|
280
|
+
for key, value in new_metadata.items():
|
|
281
|
+
book['metadata'][key] = value
|
|
282
|
+
|
|
283
|
+
payload = {
|
|
284
|
+
'uploadMetaData': book['metadata']
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
host_response = self._session.put(
|
|
288
|
+
url,
|
|
289
|
+
data=json.dumps(payload),
|
|
290
|
+
headers={
|
|
291
|
+
'content-type': 'application/json',
|
|
292
|
+
't_auth_token': self._access_token,
|
|
293
|
+
'hardware_id': self.hardware_id,
|
|
294
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not host_response.ok:
|
|
299
|
+
raise PytolinoException(f'metadata upload failed {host_response}')
|
|
300
|
+
|
|
301
|
+
def upload(self, file_path, name=None, extension=None):
|
|
302
|
+
"""upload an ebook to your cloud
|
|
303
|
+
|
|
304
|
+
:file_path: str path to the ebook to upload
|
|
305
|
+
:name: str name of book if different from filename
|
|
306
|
+
:extension: epub or pdf, if not in filename
|
|
307
|
+
:returns: epub_id on the server
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
if name is None:
|
|
312
|
+
name = file_path.split('/')[-1]
|
|
313
|
+
if extension is None:
|
|
314
|
+
extension = file_path.split('.')[-1]
|
|
315
|
+
|
|
316
|
+
mime = {
|
|
317
|
+
'pdf': 'application/pdf',
|
|
318
|
+
'epub': 'application/epub+zip',
|
|
319
|
+
}.get(extension.lower(), 'application/pdf')
|
|
320
|
+
|
|
321
|
+
host_response = self._session.post(
|
|
322
|
+
self._server_settings['upload_url'],
|
|
323
|
+
files=[(
|
|
324
|
+
'file',
|
|
325
|
+
(
|
|
326
|
+
name,
|
|
327
|
+
open(file_path, 'rb'),
|
|
328
|
+
mime,
|
|
329
|
+
),
|
|
330
|
+
)],
|
|
331
|
+
headers={
|
|
332
|
+
't_auth_token': self._access_token,
|
|
333
|
+
'hardware_id': self.hardware_id,
|
|
334
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if not host_response.ok:
|
|
339
|
+
raise PytolinoException(f'upload failed {host_response}')
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
j = host_response.json()
|
|
343
|
+
except requests.JSONDecodeError:
|
|
344
|
+
raise PytolinoException('file upload failed.')
|
|
345
|
+
else:
|
|
346
|
+
try:
|
|
347
|
+
return j['metadata']['deliverableId']
|
|
348
|
+
except KeyError:
|
|
349
|
+
raise PytolinoException('file upload failed.')
|
|
350
|
+
|
|
351
|
+
def delete_ebook(self, ebook_id):
|
|
352
|
+
"""delete an ebook present on your cloud
|
|
353
|
+
|
|
354
|
+
:ebook_id: id of the ebook to delete.
|
|
355
|
+
:returns: None
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
host_response = self._session.get(
|
|
359
|
+
self._server_settings['delete_url'],
|
|
360
|
+
params={'deliverableId': ebook_id},
|
|
361
|
+
headers={
|
|
362
|
+
't_auth_token': self._access_token,
|
|
363
|
+
'hardware_id': self.hardware_id,
|
|
364
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if not host_response.ok:
|
|
369
|
+
try:
|
|
370
|
+
j = host_response.json()
|
|
371
|
+
raise PytolinoException(
|
|
372
|
+
f"delete {ebook_id} failed: ",
|
|
373
|
+
f"{j['ResponseInfo']['message']}"
|
|
374
|
+
)
|
|
375
|
+
except KeyError:
|
|
376
|
+
raise PytolinoException(
|
|
377
|
+
f'delete {ebook_id} failed: reason unknown.')
|
|
378
|
+
|
|
379
|
+
def add_cover(self, book_id, filepath, file_ext=None):
|
|
380
|
+
"""upload a a cover to a book on the cloud
|
|
381
|
+
|
|
382
|
+
:book_id: id of the book on the serveer
|
|
383
|
+
:filepath: path to the cover file
|
|
384
|
+
:file_ext: png, jpg or jpeg. only necessary if the
|
|
385
|
+
filepath has no extension
|
|
386
|
+
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
if file_ext is None:
|
|
390
|
+
ext = filepath.split('.')[-1]
|
|
391
|
+
|
|
392
|
+
mime = {
|
|
393
|
+
'png': 'image/png',
|
|
394
|
+
'jpeg': 'image/jpeg',
|
|
395
|
+
'jpg': 'image/jpeg'
|
|
396
|
+
}.get(ext.lower(), 'application/jpeg')
|
|
397
|
+
|
|
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},
|
|
402
|
+
headers={
|
|
403
|
+
't_auth_token': self._access_token,
|
|
404
|
+
'hardware_id': self.hardware_id,
|
|
405
|
+
'reseller_id': self._server_settings['partner_id'],
|
|
406
|
+
},
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if not host_response.ok:
|
|
410
|
+
raise PytolinoException(f'cover upload failed. {host_response}')
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == '__main__':
|
|
414
|
+
main()
|