quasarr 1.20.6__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.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- quasarr/__init__.py +460 -0
- quasarr/api/__init__.py +187 -0
- quasarr/api/arr/__init__.py +373 -0
- quasarr/api/captcha/__init__.py +1075 -0
- quasarr/api/config/__init__.py +23 -0
- quasarr/api/sponsors_helper/__init__.py +166 -0
- quasarr/api/statistics/__init__.py +196 -0
- quasarr/downloads/__init__.py +267 -0
- quasarr/downloads/linkcrypters/__init__.py +0 -0
- quasarr/downloads/linkcrypters/al.py +237 -0
- quasarr/downloads/linkcrypters/filecrypt.py +444 -0
- quasarr/downloads/linkcrypters/hide.py +123 -0
- quasarr/downloads/packages/__init__.py +467 -0
- quasarr/downloads/sources/__init__.py +0 -0
- quasarr/downloads/sources/al.py +697 -0
- quasarr/downloads/sources/by.py +106 -0
- quasarr/downloads/sources/dd.py +76 -0
- quasarr/downloads/sources/dj.py +7 -0
- quasarr/downloads/sources/dt.py +66 -0
- quasarr/downloads/sources/dw.py +65 -0
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +51 -0
- quasarr/downloads/sources/nx.py +105 -0
- quasarr/downloads/sources/sf.py +159 -0
- quasarr/downloads/sources/sj.py +7 -0
- quasarr/downloads/sources/sl.py +90 -0
- quasarr/downloads/sources/wd.py +110 -0
- quasarr/providers/__init__.py +0 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +20 -0
- quasarr/providers/html_templates.py +241 -0
- quasarr/providers/imdb_metadata.py +142 -0
- quasarr/providers/log.py +19 -0
- quasarr/providers/myjd_api.py +917 -0
- quasarr/providers/notifications.py +124 -0
- quasarr/providers/obfuscated.py +51 -0
- quasarr/providers/sessions/__init__.py +0 -0
- quasarr/providers/sessions/al.py +286 -0
- quasarr/providers/sessions/dd.py +78 -0
- quasarr/providers/sessions/nx.py +76 -0
- quasarr/providers/shared_state.py +826 -0
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +118 -0
- quasarr/providers/web_server.py +49 -0
- quasarr/search/__init__.py +153 -0
- quasarr/search/sources/__init__.py +0 -0
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +203 -0
- quasarr/search/sources/dd.py +135 -0
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dt.py +265 -0
- quasarr/search/sources/dw.py +214 -0
- quasarr/search/sources/fx.py +223 -0
- quasarr/search/sources/he.py +196 -0
- quasarr/search/sources/mb.py +195 -0
- quasarr/search/sources/nk.py +188 -0
- quasarr/search/sources/nx.py +197 -0
- quasarr/search/sources/sf.py +374 -0
- quasarr/search/sources/sj.py +213 -0
- quasarr/search/sources/sl.py +246 -0
- quasarr/search/sources/wd.py +208 -0
- quasarr/storage/__init__.py +0 -0
- quasarr/storage/config.py +163 -0
- quasarr/storage/setup.py +458 -0
- quasarr/storage/sqlite_database.py +80 -0
- quasarr-1.20.6.dist-info/METADATA +304 -0
- quasarr-1.20.6.dist-info/RECORD +72 -0
- quasarr-1.20.6.dist-info/WHEEL +5 -0
- quasarr-1.20.6.dist-info/entry_points.txt +2 -0
- quasarr-1.20.6.dist-info/licenses/LICENSE +21 -0
- quasarr-1.20.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
#
|
|
5
|
+
# Original Code by:
|
|
6
|
+
# https://github.com/mmarquezs/My.Jdownloader-API-Python-Library/
|
|
7
|
+
#
|
|
8
|
+
# The MIT License (MIT)
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2015 Marc Marquez Santamaria
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
|
|
30
|
+
import base64
|
|
31
|
+
import hashlib
|
|
32
|
+
import hmac
|
|
33
|
+
import json
|
|
34
|
+
import time
|
|
35
|
+
from urllib.parse import quote
|
|
36
|
+
|
|
37
|
+
import requests
|
|
38
|
+
import urllib3
|
|
39
|
+
from Cryptodome.Cipher import AES
|
|
40
|
+
|
|
41
|
+
from quasarr.providers.log import debug
|
|
42
|
+
from quasarr.providers.version import get_version
|
|
43
|
+
|
|
44
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
45
|
+
BS = 16
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MYJDException(BaseException):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TokenExpiredException(BaseException):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RequestTimeoutException(BaseException):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pad(s):
|
|
61
|
+
return s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def unpad(s):
|
|
65
|
+
return s[0:-s[-1]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Update:
|
|
69
|
+
"""
|
|
70
|
+
Class that represents the update-functionality of a Device
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, device):
|
|
74
|
+
self.device = device
|
|
75
|
+
self.url = '/update'
|
|
76
|
+
|
|
77
|
+
def restart_and_update(self):
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
:return:
|
|
81
|
+
"""
|
|
82
|
+
resp = self.device.action(self.url + "/restartAndUpdate")
|
|
83
|
+
return resp
|
|
84
|
+
|
|
85
|
+
def run_update_check(self):
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
:return:
|
|
89
|
+
"""
|
|
90
|
+
resp = self.device.action(self.url + "/runUpdateCheck")
|
|
91
|
+
return resp
|
|
92
|
+
|
|
93
|
+
def is_update_available(self):
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
:return:
|
|
97
|
+
"""
|
|
98
|
+
resp = self.device.action(self.url + "/isUpdateAvailable")
|
|
99
|
+
return resp
|
|
100
|
+
|
|
101
|
+
def update_available(self):
|
|
102
|
+
self.run_update_check()
|
|
103
|
+
resp = self.is_update_available()
|
|
104
|
+
return resp
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Config:
|
|
108
|
+
"""
|
|
109
|
+
Class that represents the Config of a Device
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, device):
|
|
113
|
+
self.device = device
|
|
114
|
+
self.url = '/config'
|
|
115
|
+
|
|
116
|
+
def list(self):
|
|
117
|
+
"""
|
|
118
|
+
:return: List<AdvancedConfigAPIEntry>
|
|
119
|
+
"""
|
|
120
|
+
resp = self.device.action(self.url + "/list")
|
|
121
|
+
return resp
|
|
122
|
+
|
|
123
|
+
def get(self, interface_name, storage, key):
|
|
124
|
+
"""
|
|
125
|
+
:param interfaceName: a valid interface name from List<AdvancedConfigAPIEntry>
|
|
126
|
+
:type: str:
|
|
127
|
+
:param storage: 'null' to use default or 'cfg/' + interfaceName
|
|
128
|
+
:type: str:
|
|
129
|
+
:param key: a valid key from from List<AdvancedConfigAPIEntry>
|
|
130
|
+
:type: str:
|
|
131
|
+
"""
|
|
132
|
+
params = [interface_name, storage, key]
|
|
133
|
+
resp = self.device.action(self.url + "/get", params)
|
|
134
|
+
return resp
|
|
135
|
+
|
|
136
|
+
def set(self, interface_name, storage, key, value):
|
|
137
|
+
"""
|
|
138
|
+
:param interfaceName: a valid interface name from List<AdvancedConfigAPIEntry>
|
|
139
|
+
:type: str:
|
|
140
|
+
:param storage: 'null' to use default or 'cfg/' + interfaceName
|
|
141
|
+
:type: str:
|
|
142
|
+
:param key: a valid key from from List<AdvancedConfigAPIEntry>
|
|
143
|
+
:type: str:
|
|
144
|
+
:param value: a valid value for the given key (see type value from List<AdvancedConfigAPIEntry>)
|
|
145
|
+
:type: Object:
|
|
146
|
+
"""
|
|
147
|
+
params = [interface_name, storage, key, value]
|
|
148
|
+
resp = self.device.action(self.url + "/set", params)
|
|
149
|
+
return resp
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DownloadController:
|
|
153
|
+
"""
|
|
154
|
+
Class that represents the downloads-controller of a Device
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, device):
|
|
158
|
+
self.device = device
|
|
159
|
+
self.url = '/downloadcontroller'
|
|
160
|
+
|
|
161
|
+
def start_downloads(self):
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
:return:
|
|
165
|
+
"""
|
|
166
|
+
resp = self.device.action(self.url + "/start")
|
|
167
|
+
return resp
|
|
168
|
+
|
|
169
|
+
def get_current_state(self):
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
:return:
|
|
173
|
+
"""
|
|
174
|
+
resp = self.device.action(self.url + "/getCurrentState")
|
|
175
|
+
return resp
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Linkgrabber:
|
|
179
|
+
"""
|
|
180
|
+
Class that represents the linkgrabber of a Device
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, device):
|
|
184
|
+
self.device = device
|
|
185
|
+
self.url = '/linkgrabberv2'
|
|
186
|
+
|
|
187
|
+
def is_collecting(self):
|
|
188
|
+
resp = self.device.action(self.url + "/isCollecting")
|
|
189
|
+
return resp
|
|
190
|
+
|
|
191
|
+
def add_links(self,
|
|
192
|
+
params=[{
|
|
193
|
+
"autostart": True,
|
|
194
|
+
"links": None,
|
|
195
|
+
"packageName": None,
|
|
196
|
+
"extractPassword": None,
|
|
197
|
+
"priority": "DEFAULT",
|
|
198
|
+
"downloadPassword": None,
|
|
199
|
+
"destinationFolder": None,
|
|
200
|
+
"overwritePackagizerRules": False
|
|
201
|
+
}]):
|
|
202
|
+
"""
|
|
203
|
+
Add links to the linkcollector
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
"autostart" : false,
|
|
207
|
+
"links" : null,
|
|
208
|
+
"packageName" : null,
|
|
209
|
+
"extractPassword" : null,
|
|
210
|
+
"priority" : "DEFAULT",
|
|
211
|
+
"downloadPassword" : null,
|
|
212
|
+
"destinationFolder" : null
|
|
213
|
+
}
|
|
214
|
+
"""
|
|
215
|
+
resp = self.device.action(self.url + "/addLinks", params)
|
|
216
|
+
return resp
|
|
217
|
+
|
|
218
|
+
def cleanup(self,
|
|
219
|
+
action,
|
|
220
|
+
mode,
|
|
221
|
+
selection_type,
|
|
222
|
+
link_ids=[],
|
|
223
|
+
package_ids=[]):
|
|
224
|
+
"""
|
|
225
|
+
Clean packages and/or links of the linkgrabber list.
|
|
226
|
+
Requires at least a package_ids or link_ids list, or both.
|
|
227
|
+
|
|
228
|
+
:param package_ids: Package UUID's.
|
|
229
|
+
:type: list of strings.
|
|
230
|
+
:param link_ids: link UUID's.
|
|
231
|
+
:type: list of strings
|
|
232
|
+
:param action: Action to be done. Actions: DELETE_ALL, DELETE_DISABLED, DELETE_FAILED, DELETE_FINISHED, DELETE_OFFLINE, DELETE_DUPE, DELETE_MODE
|
|
233
|
+
:type: str:
|
|
234
|
+
:param mode: Mode to use. Modes: REMOVE_LINKS_AND_DELETE_FILES, REMOVE_LINKS_AND_RECYCLE_FILES, REMOVE_LINKS_ONLY
|
|
235
|
+
:type: str:
|
|
236
|
+
:param selection_type: Type of selection to use. Types: SELECTED, UNSELECTED, ALL, NONE
|
|
237
|
+
:type: str:
|
|
238
|
+
"""
|
|
239
|
+
params = [link_ids, package_ids]
|
|
240
|
+
params += [action, mode, selection_type]
|
|
241
|
+
resp = self.device.action(self.url + "/cleanup", params)
|
|
242
|
+
return resp
|
|
243
|
+
|
|
244
|
+
def remove_links(self, links_ids, packages_ids):
|
|
245
|
+
params = [links_ids, packages_ids]
|
|
246
|
+
resp = self.device.action(self.url + "/removeLinks", params)
|
|
247
|
+
return resp
|
|
248
|
+
|
|
249
|
+
def move_to_downloadlist(self, link_ids, package_ids):
|
|
250
|
+
"""
|
|
251
|
+
Moves packages and/or links to download list.
|
|
252
|
+
|
|
253
|
+
:param package_ids: Package UUID's.
|
|
254
|
+
:type: list of strings.
|
|
255
|
+
:param link_ids: Link UUID's.
|
|
256
|
+
"""
|
|
257
|
+
params = [link_ids, package_ids]
|
|
258
|
+
resp = self.device.action(self.url + "/moveToDownloadlist", params)
|
|
259
|
+
return resp
|
|
260
|
+
|
|
261
|
+
def query_links(self,
|
|
262
|
+
params=[{
|
|
263
|
+
"bytesTotal": True,
|
|
264
|
+
"comment": True,
|
|
265
|
+
"status": True,
|
|
266
|
+
"enabled": True,
|
|
267
|
+
"maxResults": -1,
|
|
268
|
+
"startAt": 0,
|
|
269
|
+
"hosts": True,
|
|
270
|
+
"url": True,
|
|
271
|
+
"availability": True,
|
|
272
|
+
"variantIcon": True,
|
|
273
|
+
"variantName": True,
|
|
274
|
+
"variantID": True,
|
|
275
|
+
"variants": True,
|
|
276
|
+
"priority": True
|
|
277
|
+
}]):
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
Get the links in the linkcollector/linkgrabber
|
|
281
|
+
|
|
282
|
+
:param params: A dictionary with options. The default dictionary is
|
|
283
|
+
configured so it returns you all the downloads with all details, but you
|
|
284
|
+
can put your own with your options
|
|
285
|
+
:type: Dictionary
|
|
286
|
+
:rtype: List of dictionaries of this style, with more or less detail based on your options.
|
|
287
|
+
"""
|
|
288
|
+
resp = self.device.action(self.url + "/queryLinks", params)
|
|
289
|
+
return resp
|
|
290
|
+
|
|
291
|
+
def query_packages(self, params=[
|
|
292
|
+
{
|
|
293
|
+
"bytesLoaded": True,
|
|
294
|
+
"bytesTotal": True,
|
|
295
|
+
"comment": True,
|
|
296
|
+
"enabled": True,
|
|
297
|
+
"eta": True,
|
|
298
|
+
"priority": False,
|
|
299
|
+
"finished": True,
|
|
300
|
+
"running": True,
|
|
301
|
+
"speed": True,
|
|
302
|
+
"status": True,
|
|
303
|
+
"childCount": True,
|
|
304
|
+
"hosts": True,
|
|
305
|
+
"saveTo": True,
|
|
306
|
+
"maxResults": -1,
|
|
307
|
+
"startAt": 0,
|
|
308
|
+
}
|
|
309
|
+
]):
|
|
310
|
+
"""
|
|
311
|
+
Get the links in the linkgrabber list
|
|
312
|
+
"""
|
|
313
|
+
resp = self.device.action("/linkgrabberv2/queryPackages", params)
|
|
314
|
+
return resp
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Downloads:
|
|
318
|
+
"""
|
|
319
|
+
Class that represents the downloads list of a Device
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def __init__(self, device):
|
|
323
|
+
self.device = device
|
|
324
|
+
self.url = "/downloadsV2"
|
|
325
|
+
|
|
326
|
+
def cleanup(self,
|
|
327
|
+
action,
|
|
328
|
+
mode,
|
|
329
|
+
selection_type,
|
|
330
|
+
link_ids=[],
|
|
331
|
+
package_ids=[]):
|
|
332
|
+
"""
|
|
333
|
+
Clean packages and/or links of the linkgrabber list.
|
|
334
|
+
Requires at least a package_ids or link_ids list, or both.
|
|
335
|
+
|
|
336
|
+
:param package_ids: Package UUID's.
|
|
337
|
+
:type: list of strings.
|
|
338
|
+
:param link_ids: link UUID's.
|
|
339
|
+
:type: list of strings
|
|
340
|
+
:param action: Action to be done. Actions: DELETE_ALL, DELETE_DISABLED, DELETE_FAILED, DELETE_FINISHED, DELETE_OFFLINE, DELETE_DUPE, DELETE_MODE
|
|
341
|
+
:type: str:
|
|
342
|
+
:param mode: Mode to use. Modes: REMOVE_LINKS_AND_DELETE_FILES, REMOVE_LINKS_AND_RECYCLE_FILES, REMOVE_LINKS_ONLY
|
|
343
|
+
:type: str:
|
|
344
|
+
:param selection_type: Type of selection to use. Types: SELECTED, UNSELECTED, ALL, NONE
|
|
345
|
+
:type: str:
|
|
346
|
+
"""
|
|
347
|
+
params = [link_ids, package_ids]
|
|
348
|
+
params += [action, mode, selection_type]
|
|
349
|
+
resp = self.device.action(self.url + "/cleanup", params)
|
|
350
|
+
return resp
|
|
351
|
+
|
|
352
|
+
def query_links(self,
|
|
353
|
+
params=[{
|
|
354
|
+
"bytesTotal": True,
|
|
355
|
+
"comment": True,
|
|
356
|
+
"status": True,
|
|
357
|
+
"enabled": True,
|
|
358
|
+
"maxResults": -1,
|
|
359
|
+
"startAt": 0,
|
|
360
|
+
"packageUUIDs": [],
|
|
361
|
+
"host": True,
|
|
362
|
+
"url": True,
|
|
363
|
+
"bytesloaded": True,
|
|
364
|
+
"speed": True,
|
|
365
|
+
"eta": True,
|
|
366
|
+
"finished": True,
|
|
367
|
+
"priority": True,
|
|
368
|
+
"running": True,
|
|
369
|
+
"skipped": True,
|
|
370
|
+
"extractionStatus": True
|
|
371
|
+
}]):
|
|
372
|
+
"""
|
|
373
|
+
Get the links in the download list
|
|
374
|
+
"""
|
|
375
|
+
resp = self.device.action(self.url + "/queryLinks", params)
|
|
376
|
+
return resp
|
|
377
|
+
|
|
378
|
+
def query_packages(self,
|
|
379
|
+
params=[{
|
|
380
|
+
"bytesLoaded": True,
|
|
381
|
+
"bytesTotal": True,
|
|
382
|
+
"comment": True,
|
|
383
|
+
"enabled": True,
|
|
384
|
+
"eta": True,
|
|
385
|
+
"priority": False,
|
|
386
|
+
"finished": True,
|
|
387
|
+
"running": True,
|
|
388
|
+
"speed": True,
|
|
389
|
+
"status": True,
|
|
390
|
+
"childCount": True,
|
|
391
|
+
"hosts": True,
|
|
392
|
+
"saveTo": True,
|
|
393
|
+
"maxResults": -1,
|
|
394
|
+
"startAt": 0,
|
|
395
|
+
}]):
|
|
396
|
+
"""
|
|
397
|
+
Get the packages in the downloads list
|
|
398
|
+
"""
|
|
399
|
+
resp = self.device.action(self.url + "/queryPackages", params)
|
|
400
|
+
return resp
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class Extraction:
|
|
404
|
+
"""
|
|
405
|
+
Class that represents the extraction details of a Device
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __init__(self, device):
|
|
409
|
+
self.device = device
|
|
410
|
+
self.url = "/extraction"
|
|
411
|
+
|
|
412
|
+
def get_archive_info(self, link_ids=[], package_ids=[]):
|
|
413
|
+
"""
|
|
414
|
+
Get ArchiveStatus for links and/or packages.
|
|
415
|
+
|
|
416
|
+
:param package_ids: Package UUID's.
|
|
417
|
+
:type: list of strings.
|
|
418
|
+
:param link_ids: link UUID's.
|
|
419
|
+
:type: list of strings
|
|
420
|
+
"""
|
|
421
|
+
params = [link_ids, package_ids]
|
|
422
|
+
resp = self.device.action(self.url + "/getArchiveInfo", params)
|
|
423
|
+
return resp
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class Jddevice:
|
|
427
|
+
"""
|
|
428
|
+
Class that represents a JDownloader device and it's functions
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def __init__(self, jd, device_dict):
|
|
432
|
+
""" This functions initializates the device instance.
|
|
433
|
+
It uses the provided dictionary to create the device.
|
|
434
|
+
|
|
435
|
+
:param device_dict: Device dictionary
|
|
436
|
+
"""
|
|
437
|
+
self.name = device_dict["name"]
|
|
438
|
+
self.device_id = device_dict["id"]
|
|
439
|
+
self.device_type = device_dict["type"]
|
|
440
|
+
self.myjd = jd
|
|
441
|
+
self.config = Config(self)
|
|
442
|
+
self.linkgrabber = Linkgrabber(self)
|
|
443
|
+
self.downloads = Downloads(self)
|
|
444
|
+
self.extraction = Extraction(self)
|
|
445
|
+
self.downloadcontroller = DownloadController(self)
|
|
446
|
+
self.update = Update(self)
|
|
447
|
+
self.__direct_connection_info = None
|
|
448
|
+
self.__refresh_direct_connections()
|
|
449
|
+
self.__direct_connection_enabled = True
|
|
450
|
+
self.__direct_connection_cooldown = 0
|
|
451
|
+
self.__direct_connection_consecutive_failures = 0
|
|
452
|
+
|
|
453
|
+
def __refresh_direct_connections(self):
|
|
454
|
+
response = self.myjd.request_api("/device/getDirectConnectionInfos",
|
|
455
|
+
"POST", None, self.__action_url())
|
|
456
|
+
if response is not None \
|
|
457
|
+
and 'data' in response \
|
|
458
|
+
and 'infos' in response["data"] \
|
|
459
|
+
and len(response["data"]["infos"]) != 0:
|
|
460
|
+
self.__update_direct_connections(response["data"]["infos"])
|
|
461
|
+
|
|
462
|
+
def __update_direct_connections(self, direct_info):
|
|
463
|
+
"""
|
|
464
|
+
Updates the direct_connections info keeping the order.
|
|
465
|
+
"""
|
|
466
|
+
tmp = []
|
|
467
|
+
if self.__direct_connection_info is None:
|
|
468
|
+
for conn in direct_info:
|
|
469
|
+
tmp.append({'conn': conn, 'cooldown': 0})
|
|
470
|
+
self.__direct_connection_info = tmp
|
|
471
|
+
return
|
|
472
|
+
# We remove old connections not available anymore.
|
|
473
|
+
for i in self.__direct_connection_info:
|
|
474
|
+
if i['conn'] not in direct_info:
|
|
475
|
+
tmp.remove(i)
|
|
476
|
+
else:
|
|
477
|
+
direct_info.remove(i['conn'])
|
|
478
|
+
# We add new connections
|
|
479
|
+
for conn in direct_info:
|
|
480
|
+
tmp.append({'conn': conn, 'cooldown': 0})
|
|
481
|
+
self.__direct_connection_info = tmp
|
|
482
|
+
|
|
483
|
+
def enable_direct_connection(self):
|
|
484
|
+
self.__direct_connection_enabled = True
|
|
485
|
+
self.__refresh_direct_connections()
|
|
486
|
+
|
|
487
|
+
def disable_direct_connection(self):
|
|
488
|
+
self.__direct_connection_enabled = False
|
|
489
|
+
self.__direct_connection_info = None
|
|
490
|
+
|
|
491
|
+
def check_direct_connection(self):
|
|
492
|
+
if self.__direct_connection_enabled and self.__direct_connection_cooldown == 0 and self.__direct_connection_consecutive_failures == 0:
|
|
493
|
+
if self.__direct_connection_info:
|
|
494
|
+
return {"status": True, "ip": self.__direct_connection_info[0]['conn']['ip']}
|
|
495
|
+
return {"status": False, "ip": None}
|
|
496
|
+
|
|
497
|
+
def action(self, path, params=(), http_action="POST"):
|
|
498
|
+
"""Execute any action in the device using the postparams and params.
|
|
499
|
+
All the info of which params are required and what are they default value, type,etc
|
|
500
|
+
can be found in the MY.Jdownloader API Specifications ( https://goo.gl/pkJ9d1 ).
|
|
501
|
+
|
|
502
|
+
:param path:
|
|
503
|
+
:param http_action:
|
|
504
|
+
:param params: Params in the url, in a list of tuples. Example:
|
|
505
|
+
/example?param1=ex¶m2=ex2 [("param1","ex"),("param2","ex2")]
|
|
506
|
+
"""
|
|
507
|
+
action_url = self.__action_url()
|
|
508
|
+
if not self.__direct_connection_enabled or self.__direct_connection_info is None \
|
|
509
|
+
or time.time() < self.__direct_connection_cooldown:
|
|
510
|
+
# No direct connection available, we use My.JDownloader api.
|
|
511
|
+
response = self.myjd.request_api(path, http_action, params,
|
|
512
|
+
action_url)
|
|
513
|
+
if response is None:
|
|
514
|
+
# My.JDownloader Api failed too.
|
|
515
|
+
return False
|
|
516
|
+
else:
|
|
517
|
+
# My.JDownloader Api worked, lets refresh the direct connections and return
|
|
518
|
+
# the response.
|
|
519
|
+
if self.__direct_connection_enabled \
|
|
520
|
+
and time.time() >= self.__direct_connection_cooldown:
|
|
521
|
+
self.__refresh_direct_connections()
|
|
522
|
+
return response['data']
|
|
523
|
+
else:
|
|
524
|
+
# Direct connection info available, we try to use it.
|
|
525
|
+
for conn in self.__direct_connection_info[:]:
|
|
526
|
+
connection_ip = conn['conn']['ip']
|
|
527
|
+
# prevent connection to internal docker ip
|
|
528
|
+
if time.time() > conn['cooldown']:
|
|
529
|
+
# We can use the connection
|
|
530
|
+
connection = conn['conn']
|
|
531
|
+
api = "http://" + connection_ip + ":" + str(
|
|
532
|
+
connection["port"])
|
|
533
|
+
try:
|
|
534
|
+
response = self.myjd.request_api(path, http_action, params,
|
|
535
|
+
action_url, api, timeout=3, output_errors=False)
|
|
536
|
+
except (TokenExpiredException, RequestTimeoutException, MYJDException):
|
|
537
|
+
response = None
|
|
538
|
+
if response is not None:
|
|
539
|
+
# This connection worked so we push it to the top of the list.
|
|
540
|
+
self.__direct_connection_info.remove(conn)
|
|
541
|
+
self.__direct_connection_info.insert(0, conn)
|
|
542
|
+
self.__direct_connection_consecutive_failures = 0
|
|
543
|
+
return response['data']
|
|
544
|
+
else:
|
|
545
|
+
# We don't try to use this connection for an hour.
|
|
546
|
+
conn['cooldown'] = time.time() + 3600
|
|
547
|
+
self.__direct_connection_info.remove(conn)
|
|
548
|
+
self.__direct_connection_info.append(conn)
|
|
549
|
+
# None of the direct connections worked, we set a cooldown for direct connections
|
|
550
|
+
self.__direct_connection_consecutive_failures += 1
|
|
551
|
+
self.__direct_connection_cooldown = time.time() + (60 * self.__direct_connection_consecutive_failures)
|
|
552
|
+
# None of the direct connections worked, we use the My.JDownloader api
|
|
553
|
+
response = self.myjd.request_api(path, http_action, params,
|
|
554
|
+
action_url)
|
|
555
|
+
if response is None:
|
|
556
|
+
# My.JDownloader Api failed too.
|
|
557
|
+
return False
|
|
558
|
+
# My.JDownloader Api worked, lets refresh the direct connections and return
|
|
559
|
+
# the response.
|
|
560
|
+
self.__refresh_direct_connections()
|
|
561
|
+
return response['data']
|
|
562
|
+
|
|
563
|
+
def __action_url(self):
|
|
564
|
+
return "/t_" + self.myjd.get_session_token() + "_" + self.device_id
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class Myjdapi:
|
|
568
|
+
"""
|
|
569
|
+
Main class for connecting to JD API.
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
def __init__(self):
|
|
574
|
+
"""
|
|
575
|
+
This functions initializates the myjd_api object.
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
self.__request_id = int(time.time() * 1000)
|
|
579
|
+
self.__api_url = "https://api.jdownloader.org"
|
|
580
|
+
self.__app_key = "quasarr"
|
|
581
|
+
self.__api_version = 1
|
|
582
|
+
self.__devices = None
|
|
583
|
+
self.__login_secret = None
|
|
584
|
+
self.__device_secret = None
|
|
585
|
+
self.__session_token = None
|
|
586
|
+
self.__regain_token = None
|
|
587
|
+
self.__server_encryption_token = None
|
|
588
|
+
self.__device_encryption_token = None
|
|
589
|
+
self.__connected = False
|
|
590
|
+
|
|
591
|
+
def get_session_token(self):
|
|
592
|
+
return self.__session_token
|
|
593
|
+
|
|
594
|
+
def is_connected(self):
|
|
595
|
+
"""
|
|
596
|
+
Indicates if there is a connection established.
|
|
597
|
+
"""
|
|
598
|
+
return self.__connected
|
|
599
|
+
|
|
600
|
+
def set_app_key(self, app_key):
|
|
601
|
+
"""
|
|
602
|
+
Sets the APP Key.
|
|
603
|
+
"""
|
|
604
|
+
self.__app_key = app_key
|
|
605
|
+
|
|
606
|
+
def __secret_create(self, email, password, domain):
|
|
607
|
+
"""
|
|
608
|
+
Calculates the login_secret and device_secret
|
|
609
|
+
|
|
610
|
+
:param email: My.Jdownloader User email
|
|
611
|
+
:param password: My.Jdownloader User password
|
|
612
|
+
:param domain: The domain , if is for Server (login_secret) or Device (device_secret)
|
|
613
|
+
:return: secret hash
|
|
614
|
+
|
|
615
|
+
"""
|
|
616
|
+
secret_hash = hashlib.sha256()
|
|
617
|
+
secret_hash.update(email.lower().encode('utf-8')
|
|
618
|
+
+ password.encode('utf-8')
|
|
619
|
+
+ domain.lower().encode('utf-8'))
|
|
620
|
+
return secret_hash.digest()
|
|
621
|
+
|
|
622
|
+
def __update_encryption_tokens(self):
|
|
623
|
+
"""
|
|
624
|
+
Updates the server_encryption_token and device_encryption_token
|
|
625
|
+
|
|
626
|
+
"""
|
|
627
|
+
if self.__server_encryption_token is None:
|
|
628
|
+
old_token = self.__login_secret
|
|
629
|
+
else:
|
|
630
|
+
old_token = self.__server_encryption_token
|
|
631
|
+
new_token = hashlib.sha256()
|
|
632
|
+
new_token.update(old_token + bytearray.fromhex(self.__session_token))
|
|
633
|
+
self.__server_encryption_token = new_token.digest()
|
|
634
|
+
new_token = hashlib.sha256()
|
|
635
|
+
new_token.update(self.__device_secret +
|
|
636
|
+
bytearray.fromhex(self.__session_token))
|
|
637
|
+
self.__device_encryption_token = new_token.digest()
|
|
638
|
+
|
|
639
|
+
def __signature_create(self, key, data):
|
|
640
|
+
"""
|
|
641
|
+
Calculates the signature for the data given a key.
|
|
642
|
+
|
|
643
|
+
:param key:
|
|
644
|
+
:param data:
|
|
645
|
+
"""
|
|
646
|
+
signature = hmac.new(key, data.encode('utf-8'), hashlib.sha256)
|
|
647
|
+
return signature.hexdigest()
|
|
648
|
+
|
|
649
|
+
def __decrypt(self, secret_token, data):
|
|
650
|
+
"""
|
|
651
|
+
Decrypts the data from the server using the provided token
|
|
652
|
+
|
|
653
|
+
:param secret_token:
|
|
654
|
+
:param data:
|
|
655
|
+
"""
|
|
656
|
+
init_vector = secret_token[: len(secret_token) // 2]
|
|
657
|
+
key = secret_token[len(secret_token) // 2:]
|
|
658
|
+
decryptor = AES.new(key, AES.MODE_CBC, init_vector)
|
|
659
|
+
try:
|
|
660
|
+
decrypted_data = unpad(decryptor.decrypt(self.__base64_decode(data)))
|
|
661
|
+
except:
|
|
662
|
+
raise MYJDException(
|
|
663
|
+
"Failed to decode response: {}", data
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
return decrypted_data
|
|
667
|
+
|
|
668
|
+
def __base64_decode(self, s):
|
|
669
|
+
"""Add missing padding to string and return the decoded base64 string."""
|
|
670
|
+
s = str(s).strip()
|
|
671
|
+
try:
|
|
672
|
+
return base64.b64decode(s)
|
|
673
|
+
except TypeError:
|
|
674
|
+
padding = len(s) % 4
|
|
675
|
+
if padding == 1:
|
|
676
|
+
return ""
|
|
677
|
+
elif padding == 2:
|
|
678
|
+
s += b"=="
|
|
679
|
+
elif padding == 3:
|
|
680
|
+
s += b"="
|
|
681
|
+
return base64.b64decode(s)
|
|
682
|
+
|
|
683
|
+
def __encrypt(self, secret_token, data):
|
|
684
|
+
"""
|
|
685
|
+
Encrypts the data from the server using the provided token
|
|
686
|
+
|
|
687
|
+
:param secret_token:
|
|
688
|
+
:param data:
|
|
689
|
+
"""
|
|
690
|
+
data = pad(data.encode('utf-8'))
|
|
691
|
+
init_vector = secret_token[:len(secret_token) // 2]
|
|
692
|
+
key = secret_token[len(secret_token) // 2:]
|
|
693
|
+
encryptor = AES.new(key, AES.MODE_CBC, init_vector)
|
|
694
|
+
encrypted_data = base64.b64encode(encryptor.encrypt(data))
|
|
695
|
+
return encrypted_data.decode('utf-8')
|
|
696
|
+
|
|
697
|
+
def update_request_id(self):
|
|
698
|
+
"""
|
|
699
|
+
Updates Request_Id
|
|
700
|
+
"""
|
|
701
|
+
self.__request_id = int(time.time())
|
|
702
|
+
|
|
703
|
+
def connect(self, email, password):
|
|
704
|
+
"""Establish connection to api
|
|
705
|
+
|
|
706
|
+
:param email: My.Jdownloader User email
|
|
707
|
+
:param password: My.Jdownloader User password
|
|
708
|
+
:returns: boolean -- True if succesful, False if there was any error.
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
self.update_request_id()
|
|
712
|
+
self.__login_secret = None
|
|
713
|
+
self.__device_secret = None
|
|
714
|
+
self.__session_token = None
|
|
715
|
+
self.__regain_token = None
|
|
716
|
+
self.__server_encryption_token = None
|
|
717
|
+
self.__device_encryption_token = None
|
|
718
|
+
self.__devices = None
|
|
719
|
+
self.__connected = False
|
|
720
|
+
|
|
721
|
+
self.__login_secret = self.__secret_create(email, password, "server")
|
|
722
|
+
self.__device_secret = self.__secret_create(email, password, "device")
|
|
723
|
+
response = self.request_api("/my/connect", "GET", [("email", email),
|
|
724
|
+
("appkey",
|
|
725
|
+
self.__app_key)])
|
|
726
|
+
self.__connected = True
|
|
727
|
+
self.update_request_id()
|
|
728
|
+
self.__session_token = response["sessiontoken"]
|
|
729
|
+
self.__regain_token = response["regaintoken"]
|
|
730
|
+
self.__update_encryption_tokens()
|
|
731
|
+
self.update_devices()
|
|
732
|
+
return response
|
|
733
|
+
|
|
734
|
+
def update_devices(self):
|
|
735
|
+
"""
|
|
736
|
+
Updates available devices. Use list_devices() to get the devices list.
|
|
737
|
+
|
|
738
|
+
:returns: boolean -- True if successful, False if there was any error.
|
|
739
|
+
"""
|
|
740
|
+
response = self.request_api("/my/listdevices", "GET",
|
|
741
|
+
[("sessiontoken", self.__session_token)])
|
|
742
|
+
self.update_request_id()
|
|
743
|
+
self.__devices = response["list"]
|
|
744
|
+
|
|
745
|
+
def list_devices(self):
|
|
746
|
+
"""
|
|
747
|
+
Returns available devices. Use getDevices() to update the devices list.
|
|
748
|
+
Each device in the list is a dictionary like this example:
|
|
749
|
+
|
|
750
|
+
{
|
|
751
|
+
'name': 'Device',
|
|
752
|
+
'id': 'af9d03a21ddb917492dc1af8a6427f11',
|
|
753
|
+
'type': 'jd'
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
:returns: list -- list of devices.
|
|
757
|
+
"""
|
|
758
|
+
return self.__devices
|
|
759
|
+
|
|
760
|
+
def get_device(self, device_name=None, device_id=None):
|
|
761
|
+
"""
|
|
762
|
+
Returns a jddevice instance of the device
|
|
763
|
+
:param device_name:
|
|
764
|
+
:param device_id:
|
|
765
|
+
|
|
766
|
+
"""
|
|
767
|
+
if not self.is_connected():
|
|
768
|
+
raise (MYJDException("No connection established\n"))
|
|
769
|
+
if device_id is not None:
|
|
770
|
+
for device in self.__devices:
|
|
771
|
+
if device["id"] == device_id:
|
|
772
|
+
return Jddevice(self, device)
|
|
773
|
+
elif device_name is not None:
|
|
774
|
+
for device in self.__devices:
|
|
775
|
+
if device["name"] == device_name:
|
|
776
|
+
return Jddevice(self, device)
|
|
777
|
+
raise (MYJDException("Device not found\n"))
|
|
778
|
+
|
|
779
|
+
def request_api(self,
|
|
780
|
+
path,
|
|
781
|
+
http_method="GET",
|
|
782
|
+
params=None,
|
|
783
|
+
action=None,
|
|
784
|
+
api=None,
|
|
785
|
+
timeout=30,
|
|
786
|
+
output_errors=True):
|
|
787
|
+
"""
|
|
788
|
+
Makes a request to the API to the 'path' using the 'http_method' with parameters,'params'.
|
|
789
|
+
Ex:
|
|
790
|
+
http_method=GET
|
|
791
|
+
params={"test":"test"}
|
|
792
|
+
post_params={"test2":"test2"}
|
|
793
|
+
action=True
|
|
794
|
+
This would make a request to "https://api.jdownloader.org"
|
|
795
|
+
"""
|
|
796
|
+
if not api:
|
|
797
|
+
api = self.__api_url
|
|
798
|
+
data = None
|
|
799
|
+
if not self.is_connected() and path != "/my/connect":
|
|
800
|
+
raise (MYJDException("No connection established\n"))
|
|
801
|
+
if http_method == "GET":
|
|
802
|
+
query = [path + "?"]
|
|
803
|
+
if params is not None:
|
|
804
|
+
for param in params:
|
|
805
|
+
if param[0] != "encryptedLoginSecret":
|
|
806
|
+
query += [f"{param[0]}={quote(param[1])}"]
|
|
807
|
+
else:
|
|
808
|
+
query += [f"&{param[0]}={param[1]}"]
|
|
809
|
+
query += ["rid=" + str(self.__request_id)]
|
|
810
|
+
if self.__server_encryption_token is None:
|
|
811
|
+
query += [
|
|
812
|
+
"signature="
|
|
813
|
+
+ str(self.__signature_create(self.__login_secret,
|
|
814
|
+
query[0] + "&".join(query[1:])))
|
|
815
|
+
]
|
|
816
|
+
else:
|
|
817
|
+
query += [
|
|
818
|
+
"signature="
|
|
819
|
+
+ str(self.__signature_create(self.__server_encryption_token,
|
|
820
|
+
query[0] + "&".join(query[1:])))
|
|
821
|
+
]
|
|
822
|
+
query = query[0] + "&".join(query[1:])
|
|
823
|
+
|
|
824
|
+
headers = {
|
|
825
|
+
"User-Agent": f"Quasarr/{get_version()}"
|
|
826
|
+
}
|
|
827
|
+
try:
|
|
828
|
+
encrypted_response = requests.get(api + query, timeout=timeout, headers=headers)
|
|
829
|
+
except Exception:
|
|
830
|
+
encrypted_response = requests.get(api + query, timeout=timeout, headers=headers, verify=False)
|
|
831
|
+
debug("Could not establish secure connection to JDownloader.")
|
|
832
|
+
else:
|
|
833
|
+
params_request = []
|
|
834
|
+
if params is not None:
|
|
835
|
+
for param in params:
|
|
836
|
+
if not isinstance(param, list):
|
|
837
|
+
params_request += [json.dumps(param)]
|
|
838
|
+
else:
|
|
839
|
+
params_request += [param]
|
|
840
|
+
params_request = {
|
|
841
|
+
"apiVer": self.__api_version,
|
|
842
|
+
"url": path,
|
|
843
|
+
"params": params_request,
|
|
844
|
+
"rid": self.__request_id
|
|
845
|
+
}
|
|
846
|
+
data = json.dumps(params_request)
|
|
847
|
+
# Removing quotes around null elements.
|
|
848
|
+
data = data.replace('"null"', "null")
|
|
849
|
+
data = data.replace("'null'", "null")
|
|
850
|
+
encrypted_data = self.__encrypt(self.__device_encryption_token,
|
|
851
|
+
data)
|
|
852
|
+
if action is not None:
|
|
853
|
+
request_url = api + action + path
|
|
854
|
+
else:
|
|
855
|
+
request_url = api + path
|
|
856
|
+
try:
|
|
857
|
+
encrypted_response = requests.post(
|
|
858
|
+
request_url,
|
|
859
|
+
headers={
|
|
860
|
+
"Content-Type": "application/aesjson-jd; charset=utf-8",
|
|
861
|
+
"User-Agent": f"Quasarr/{get_version()}"
|
|
862
|
+
},
|
|
863
|
+
data=encrypted_data,
|
|
864
|
+
timeout=timeout
|
|
865
|
+
)
|
|
866
|
+
except Exception:
|
|
867
|
+
try:
|
|
868
|
+
encrypted_response = requests.post(
|
|
869
|
+
request_url,
|
|
870
|
+
headers={
|
|
871
|
+
"Content-Type": "application/aesjson-jd; charset=utf-8",
|
|
872
|
+
"User-Agent": f"Quasarr/{get_version()}"
|
|
873
|
+
},
|
|
874
|
+
data=encrypted_data,
|
|
875
|
+
timeout=timeout,
|
|
876
|
+
verify=False
|
|
877
|
+
)
|
|
878
|
+
debug("Could not establish secure connection to JDownloader.")
|
|
879
|
+
except Exception:
|
|
880
|
+
return None
|
|
881
|
+
if encrypted_response.status_code == 403:
|
|
882
|
+
raise TokenExpiredException
|
|
883
|
+
if encrypted_response.status_code == 503:
|
|
884
|
+
raise RequestTimeoutException
|
|
885
|
+
if encrypted_response.status_code != 200:
|
|
886
|
+
try:
|
|
887
|
+
error_msg = json.loads(encrypted_response.text)
|
|
888
|
+
except:
|
|
889
|
+
try:
|
|
890
|
+
error_msg = json.loads(self.__decrypt(self.__device_encryption_token, encrypted_response.text))
|
|
891
|
+
except:
|
|
892
|
+
raise MYJDException("Failed to decode response: {}", encrypted_response.text)
|
|
893
|
+
msg = "\n\tSOURCE: " + error_msg["src"] + "\n\tTYPE: " + \
|
|
894
|
+
error_msg["type"] + "\n------\nREQUEST_URL: " + \
|
|
895
|
+
api + path
|
|
896
|
+
if http_method == "GET":
|
|
897
|
+
msg += query
|
|
898
|
+
msg += "\n"
|
|
899
|
+
if data is not None:
|
|
900
|
+
msg += "DATA:\n" + data
|
|
901
|
+
raise (MYJDException(msg))
|
|
902
|
+
if action is None:
|
|
903
|
+
if not self.__server_encryption_token:
|
|
904
|
+
response = self.__decrypt(self.__login_secret,
|
|
905
|
+
encrypted_response.text)
|
|
906
|
+
else:
|
|
907
|
+
response = self.__decrypt(self.__server_encryption_token,
|
|
908
|
+
encrypted_response.text)
|
|
909
|
+
else:
|
|
910
|
+
response = self.__decrypt(self.__device_encryption_token,
|
|
911
|
+
encrypted_response.text)
|
|
912
|
+
jsondata = json.loads(response.decode('utf-8'))
|
|
913
|
+
if jsondata['rid'] != self.__request_id:
|
|
914
|
+
self.update_request_id()
|
|
915
|
+
return None
|
|
916
|
+
self.update_request_id()
|
|
917
|
+
return jsondata
|