quasarr 2.4.7__py3-none-any.whl → 2.4.9__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.
- quasarr/__init__.py +134 -70
- quasarr/api/__init__.py +40 -31
- quasarr/api/arr/__init__.py +116 -108
- quasarr/api/captcha/__init__.py +262 -137
- quasarr/api/config/__init__.py +76 -46
- quasarr/api/packages/__init__.py +138 -102
- quasarr/api/sponsors_helper/__init__.py +29 -16
- quasarr/api/statistics/__init__.py +19 -19
- quasarr/downloads/__init__.py +165 -72
- quasarr/downloads/linkcrypters/al.py +35 -18
- quasarr/downloads/linkcrypters/filecrypt.py +107 -52
- quasarr/downloads/linkcrypters/hide.py +5 -6
- quasarr/downloads/packages/__init__.py +342 -177
- quasarr/downloads/sources/al.py +191 -100
- quasarr/downloads/sources/by.py +31 -13
- quasarr/downloads/sources/dd.py +27 -14
- quasarr/downloads/sources/dj.py +1 -3
- quasarr/downloads/sources/dl.py +126 -71
- quasarr/downloads/sources/dt.py +11 -5
- quasarr/downloads/sources/dw.py +28 -14
- quasarr/downloads/sources/he.py +32 -24
- quasarr/downloads/sources/mb.py +19 -9
- quasarr/downloads/sources/nk.py +14 -10
- quasarr/downloads/sources/nx.py +8 -18
- quasarr/downloads/sources/sf.py +45 -20
- quasarr/downloads/sources/sj.py +1 -3
- quasarr/downloads/sources/sl.py +9 -5
- quasarr/downloads/sources/wd.py +32 -12
- quasarr/downloads/sources/wx.py +35 -21
- quasarr/providers/auth.py +42 -37
- quasarr/providers/cloudflare.py +28 -30
- quasarr/providers/hostname_issues.py +2 -1
- quasarr/providers/html_images.py +2 -2
- quasarr/providers/html_templates.py +22 -14
- quasarr/providers/imdb_metadata.py +149 -80
- quasarr/providers/jd_cache.py +131 -39
- quasarr/providers/log.py +1 -1
- quasarr/providers/myjd_api.py +260 -196
- quasarr/providers/notifications.py +53 -41
- quasarr/providers/obfuscated.py +9 -4
- quasarr/providers/sessions/al.py +71 -55
- quasarr/providers/sessions/dd.py +21 -14
- quasarr/providers/sessions/dl.py +30 -19
- quasarr/providers/sessions/nx.py +23 -14
- quasarr/providers/shared_state.py +292 -141
- quasarr/providers/statistics.py +75 -43
- quasarr/providers/utils.py +33 -27
- quasarr/providers/version.py +45 -14
- quasarr/providers/web_server.py +10 -5
- quasarr/search/__init__.py +30 -18
- quasarr/search/sources/al.py +124 -73
- quasarr/search/sources/by.py +110 -59
- quasarr/search/sources/dd.py +57 -35
- quasarr/search/sources/dj.py +69 -48
- quasarr/search/sources/dl.py +159 -100
- quasarr/search/sources/dt.py +110 -74
- quasarr/search/sources/dw.py +121 -61
- quasarr/search/sources/fx.py +108 -62
- quasarr/search/sources/he.py +78 -49
- quasarr/search/sources/mb.py +96 -48
- quasarr/search/sources/nk.py +80 -50
- quasarr/search/sources/nx.py +91 -62
- quasarr/search/sources/sf.py +171 -106
- quasarr/search/sources/sj.py +69 -48
- quasarr/search/sources/sl.py +115 -71
- quasarr/search/sources/wd.py +67 -44
- quasarr/search/sources/wx.py +188 -123
- quasarr/storage/config.py +65 -52
- quasarr/storage/setup.py +238 -140
- quasarr/storage/sqlite_database.py +10 -4
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
- quasarr-2.4.9.dist-info/RECORD +81 -0
- quasarr-2.4.7.dist-info/RECORD +0 -81
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/providers/myjd_api.py
CHANGED
|
@@ -38,7 +38,7 @@ import requests
|
|
|
38
38
|
import urllib3
|
|
39
39
|
from Cryptodome.Cipher import AES
|
|
40
40
|
|
|
41
|
-
from quasarr.providers.log import
|
|
41
|
+
from quasarr.providers.log import debug, info
|
|
42
42
|
from quasarr.providers.version import get_version
|
|
43
43
|
|
|
44
44
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
@@ -62,7 +62,7 @@ def pad(s):
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def unpad(s):
|
|
65
|
-
return s[0
|
|
65
|
+
return s[0 : -s[-1]]
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
class Update:
|
|
@@ -72,7 +72,7 @@ class Update:
|
|
|
72
72
|
|
|
73
73
|
def __init__(self, device):
|
|
74
74
|
self.device = device
|
|
75
|
-
self.url =
|
|
75
|
+
self.url = "/update"
|
|
76
76
|
|
|
77
77
|
def restart_and_update(self):
|
|
78
78
|
"""
|
|
@@ -111,7 +111,7 @@ class Config:
|
|
|
111
111
|
|
|
112
112
|
def __init__(self, device):
|
|
113
113
|
self.device = device
|
|
114
|
-
self.url =
|
|
114
|
+
self.url = "/config"
|
|
115
115
|
|
|
116
116
|
def list(self):
|
|
117
117
|
"""
|
|
@@ -156,7 +156,7 @@ class DownloadController:
|
|
|
156
156
|
|
|
157
157
|
def __init__(self, device):
|
|
158
158
|
self.device = device
|
|
159
|
-
self.url =
|
|
159
|
+
self.url = "/downloadcontroller"
|
|
160
160
|
|
|
161
161
|
def start_downloads(self):
|
|
162
162
|
"""
|
|
@@ -182,23 +182,27 @@ class Linkgrabber:
|
|
|
182
182
|
|
|
183
183
|
def __init__(self, device):
|
|
184
184
|
self.device = device
|
|
185
|
-
self.url =
|
|
185
|
+
self.url = "/linkgrabberv2"
|
|
186
186
|
|
|
187
187
|
def is_collecting(self):
|
|
188
188
|
resp = self.device.action(self.url + "/isCollecting")
|
|
189
189
|
return resp
|
|
190
190
|
|
|
191
|
-
def add_links(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
def add_links(
|
|
192
|
+
self,
|
|
193
|
+
params=[
|
|
194
|
+
{
|
|
195
|
+
"autostart": True,
|
|
196
|
+
"links": None,
|
|
197
|
+
"packageName": None,
|
|
198
|
+
"extractPassword": None,
|
|
199
|
+
"priority": "DEFAULT",
|
|
200
|
+
"downloadPassword": None,
|
|
201
|
+
"destinationFolder": None,
|
|
202
|
+
"overwritePackagizerRules": False,
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
):
|
|
202
206
|
"""
|
|
203
207
|
Add links to the linkcollector
|
|
204
208
|
|
|
@@ -215,12 +219,7 @@ class Linkgrabber:
|
|
|
215
219
|
resp = self.device.action(self.url + "/addLinks", params)
|
|
216
220
|
return resp
|
|
217
221
|
|
|
218
|
-
def cleanup(self,
|
|
219
|
-
action,
|
|
220
|
-
mode,
|
|
221
|
-
selection_type,
|
|
222
|
-
link_ids=[],
|
|
223
|
-
package_ids=[]):
|
|
222
|
+
def cleanup(self, action, mode, selection_type, link_ids=[], package_ids=[]):
|
|
224
223
|
"""
|
|
225
224
|
Clean packages and/or links of the linkgrabber list.
|
|
226
225
|
Requires at least a package_ids or link_ids list, or both.
|
|
@@ -258,23 +257,27 @@ class Linkgrabber:
|
|
|
258
257
|
resp = self.device.action(self.url + "/moveToDownloadlist", params)
|
|
259
258
|
return resp
|
|
260
259
|
|
|
261
|
-
def query_links(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
260
|
+
def query_links(
|
|
261
|
+
self,
|
|
262
|
+
params=[
|
|
263
|
+
{
|
|
264
|
+
"bytesTotal": True,
|
|
265
|
+
"comment": True,
|
|
266
|
+
"status": True,
|
|
267
|
+
"enabled": True,
|
|
268
|
+
"maxResults": -1,
|
|
269
|
+
"startAt": 0,
|
|
270
|
+
"hosts": True,
|
|
271
|
+
"url": True,
|
|
272
|
+
"availability": True,
|
|
273
|
+
"variantIcon": True,
|
|
274
|
+
"variantName": True,
|
|
275
|
+
"variantID": True,
|
|
276
|
+
"variants": True,
|
|
277
|
+
"priority": True,
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
):
|
|
278
281
|
"""
|
|
279
282
|
|
|
280
283
|
Get the links in the linkcollector/linkgrabber
|
|
@@ -288,25 +291,28 @@ class Linkgrabber:
|
|
|
288
291
|
resp = self.device.action(self.url + "/queryLinks", params)
|
|
289
292
|
return resp
|
|
290
293
|
|
|
291
|
-
def query_packages(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
294
|
+
def query_packages(
|
|
295
|
+
self,
|
|
296
|
+
params=[
|
|
297
|
+
{
|
|
298
|
+
"bytesLoaded": True,
|
|
299
|
+
"bytesTotal": True,
|
|
300
|
+
"comment": True,
|
|
301
|
+
"enabled": True,
|
|
302
|
+
"eta": True,
|
|
303
|
+
"priority": False,
|
|
304
|
+
"finished": True,
|
|
305
|
+
"running": True,
|
|
306
|
+
"speed": True,
|
|
307
|
+
"status": True,
|
|
308
|
+
"childCount": True,
|
|
309
|
+
"hosts": True,
|
|
310
|
+
"saveTo": True,
|
|
311
|
+
"maxResults": -1,
|
|
312
|
+
"startAt": 0,
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
):
|
|
310
316
|
"""
|
|
311
317
|
Get the links in the linkgrabber list
|
|
312
318
|
"""
|
|
@@ -323,12 +329,7 @@ class Downloads:
|
|
|
323
329
|
self.device = device
|
|
324
330
|
self.url = "/downloadsV2"
|
|
325
331
|
|
|
326
|
-
def cleanup(self,
|
|
327
|
-
action,
|
|
328
|
-
mode,
|
|
329
|
-
selection_type,
|
|
330
|
-
link_ids=[],
|
|
331
|
-
package_ids=[]):
|
|
332
|
+
def cleanup(self, action, mode, selection_type, link_ids=[], package_ids=[]):
|
|
332
333
|
"""
|
|
333
334
|
Clean packages and/or links of the linkgrabber list.
|
|
334
335
|
Requires at least a package_ids or link_ids list, or both.
|
|
@@ -349,50 +350,58 @@ class Downloads:
|
|
|
349
350
|
resp = self.device.action(self.url + "/cleanup", params)
|
|
350
351
|
return resp
|
|
351
352
|
|
|
352
|
-
def query_links(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
353
|
+
def query_links(
|
|
354
|
+
self,
|
|
355
|
+
params=[
|
|
356
|
+
{
|
|
357
|
+
"bytesTotal": True,
|
|
358
|
+
"comment": True,
|
|
359
|
+
"status": True,
|
|
360
|
+
"enabled": True,
|
|
361
|
+
"maxResults": -1,
|
|
362
|
+
"startAt": 0,
|
|
363
|
+
"packageUUIDs": [],
|
|
364
|
+
"host": True,
|
|
365
|
+
"url": True,
|
|
366
|
+
"bytesloaded": True,
|
|
367
|
+
"speed": True,
|
|
368
|
+
"eta": True,
|
|
369
|
+
"finished": True,
|
|
370
|
+
"priority": True,
|
|
371
|
+
"running": True,
|
|
372
|
+
"skipped": True,
|
|
373
|
+
"extractionStatus": True,
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
):
|
|
372
377
|
"""
|
|
373
378
|
Get the links in the download list
|
|
374
379
|
"""
|
|
375
380
|
resp = self.device.action(self.url + "/queryLinks", params)
|
|
376
381
|
return resp
|
|
377
382
|
|
|
378
|
-
def query_packages(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
383
|
+
def query_packages(
|
|
384
|
+
self,
|
|
385
|
+
params=[
|
|
386
|
+
{
|
|
387
|
+
"bytesLoaded": True,
|
|
388
|
+
"bytesTotal": True,
|
|
389
|
+
"comment": True,
|
|
390
|
+
"enabled": True,
|
|
391
|
+
"eta": True,
|
|
392
|
+
"priority": False,
|
|
393
|
+
"finished": True,
|
|
394
|
+
"running": True,
|
|
395
|
+
"speed": True,
|
|
396
|
+
"status": True,
|
|
397
|
+
"childCount": True,
|
|
398
|
+
"hosts": True,
|
|
399
|
+
"saveTo": True,
|
|
400
|
+
"maxResults": -1,
|
|
401
|
+
"startAt": 0,
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
):
|
|
396
405
|
"""
|
|
397
406
|
Get the packages in the downloads list
|
|
398
407
|
"""
|
|
@@ -429,7 +438,7 @@ class Jddevice:
|
|
|
429
438
|
"""
|
|
430
439
|
|
|
431
440
|
def __init__(self, jd, device_dict):
|
|
432
|
-
"""
|
|
441
|
+
"""This functions initializates the device instance.
|
|
433
442
|
It uses the provided dictionary to create the device.
|
|
434
443
|
|
|
435
444
|
:param device_dict: Device dictionary
|
|
@@ -451,12 +460,15 @@ class Jddevice:
|
|
|
451
460
|
self.__direct_connection_consecutive_failures = 0
|
|
452
461
|
|
|
453
462
|
def __refresh_direct_connections(self):
|
|
454
|
-
response = self.myjd.request_api(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
463
|
+
response = self.myjd.request_api(
|
|
464
|
+
"/device/getDirectConnectionInfos", "POST", None, self.__action_url()
|
|
465
|
+
)
|
|
466
|
+
if (
|
|
467
|
+
response is not None
|
|
468
|
+
and "data" in response
|
|
469
|
+
and "infos" in response["data"]
|
|
470
|
+
and len(response["data"]["infos"]) != 0
|
|
471
|
+
):
|
|
460
472
|
self.__update_direct_connections(response["data"]["infos"])
|
|
461
473
|
|
|
462
474
|
def __update_direct_connections(self, direct_info):
|
|
@@ -466,18 +478,18 @@ class Jddevice:
|
|
|
466
478
|
tmp = []
|
|
467
479
|
if self.__direct_connection_info is None:
|
|
468
480
|
for conn in direct_info:
|
|
469
|
-
tmp.append({
|
|
481
|
+
tmp.append({"conn": conn, "cooldown": 0})
|
|
470
482
|
self.__direct_connection_info = tmp
|
|
471
483
|
return
|
|
472
484
|
# We remove old connections not available anymore.
|
|
473
485
|
for i in self.__direct_connection_info:
|
|
474
|
-
if i[
|
|
486
|
+
if i["conn"] not in direct_info:
|
|
475
487
|
tmp.remove(i)
|
|
476
488
|
else:
|
|
477
|
-
direct_info.remove(i[
|
|
489
|
+
direct_info.remove(i["conn"])
|
|
478
490
|
# We add new connections
|
|
479
491
|
for conn in direct_info:
|
|
480
|
-
tmp.append({
|
|
492
|
+
tmp.append({"conn": conn, "cooldown": 0})
|
|
481
493
|
self.__direct_connection_info = tmp
|
|
482
494
|
|
|
483
495
|
def enable_direct_connection(self):
|
|
@@ -489,9 +501,16 @@ class Jddevice:
|
|
|
489
501
|
self.__direct_connection_info = None
|
|
490
502
|
|
|
491
503
|
def check_direct_connection(self):
|
|
492
|
-
if
|
|
504
|
+
if (
|
|
505
|
+
self.__direct_connection_enabled
|
|
506
|
+
and self.__direct_connection_cooldown == 0
|
|
507
|
+
and self.__direct_connection_consecutive_failures == 0
|
|
508
|
+
):
|
|
493
509
|
if self.__direct_connection_info:
|
|
494
|
-
return {
|
|
510
|
+
return {
|
|
511
|
+
"status": True,
|
|
512
|
+
"ip": self.__direct_connection_info[0]["conn"]["ip"],
|
|
513
|
+
}
|
|
495
514
|
return {"status": False, "ip": None}
|
|
496
515
|
|
|
497
516
|
def action(self, path, params=(), http_action="POST"):
|
|
@@ -505,60 +524,75 @@ class Jddevice:
|
|
|
505
524
|
/example?param1=ex¶m2=ex2 [("param1","ex"),("param2","ex2")]
|
|
506
525
|
"""
|
|
507
526
|
action_url = self.__action_url()
|
|
508
|
-
if
|
|
509
|
-
|
|
527
|
+
if (
|
|
528
|
+
not self.__direct_connection_enabled
|
|
529
|
+
or self.__direct_connection_info is None
|
|
530
|
+
or time.time() < self.__direct_connection_cooldown
|
|
531
|
+
):
|
|
510
532
|
# No direct connection available, we use My.JDownloader api.
|
|
511
|
-
response = self.myjd.request_api(path, http_action, params,
|
|
512
|
-
action_url)
|
|
533
|
+
response = self.myjd.request_api(path, http_action, params, action_url)
|
|
513
534
|
if response is None:
|
|
514
535
|
# My.JDownloader Api failed too.
|
|
515
536
|
return False
|
|
516
537
|
else:
|
|
517
538
|
# My.JDownloader Api worked, lets refresh the direct connections and return
|
|
518
539
|
# the response.
|
|
519
|
-
if
|
|
520
|
-
|
|
540
|
+
if (
|
|
541
|
+
self.__direct_connection_enabled
|
|
542
|
+
and time.time() >= self.__direct_connection_cooldown
|
|
543
|
+
):
|
|
521
544
|
self.__refresh_direct_connections()
|
|
522
|
-
return response[
|
|
545
|
+
return response["data"]
|
|
523
546
|
else:
|
|
524
547
|
# Direct connection info available, we try to use it.
|
|
525
548
|
for conn in self.__direct_connection_info[:]:
|
|
526
|
-
connection_ip = conn[
|
|
549
|
+
connection_ip = conn["conn"]["ip"]
|
|
527
550
|
# prevent connection to internal docker ip
|
|
528
|
-
if time.time() > conn[
|
|
551
|
+
if time.time() > conn["cooldown"]:
|
|
529
552
|
# We can use the connection
|
|
530
|
-
connection = conn[
|
|
531
|
-
api = "http://" + connection_ip + ":" + str(
|
|
532
|
-
connection["port"])
|
|
553
|
+
connection = conn["conn"]
|
|
554
|
+
api = "http://" + connection_ip + ":" + str(connection["port"])
|
|
533
555
|
try:
|
|
534
|
-
response = self.myjd.request_api(
|
|
535
|
-
|
|
536
|
-
|
|
556
|
+
response = self.myjd.request_api(
|
|
557
|
+
path,
|
|
558
|
+
http_action,
|
|
559
|
+
params,
|
|
560
|
+
action_url,
|
|
561
|
+
api,
|
|
562
|
+
timeout=3,
|
|
563
|
+
output_errors=False,
|
|
564
|
+
)
|
|
565
|
+
except (
|
|
566
|
+
TokenExpiredException,
|
|
567
|
+
RequestTimeoutException,
|
|
568
|
+
MYJDException,
|
|
569
|
+
):
|
|
537
570
|
response = None
|
|
538
571
|
if response is not None:
|
|
539
572
|
# This connection worked so we push it to the top of the list.
|
|
540
573
|
self.__direct_connection_info.remove(conn)
|
|
541
574
|
self.__direct_connection_info.insert(0, conn)
|
|
542
575
|
self.__direct_connection_consecutive_failures = 0
|
|
543
|
-
return response[
|
|
576
|
+
return response["data"]
|
|
544
577
|
else:
|
|
545
578
|
# We don't try to use this connection for an hour.
|
|
546
|
-
conn[
|
|
579
|
+
conn["cooldown"] = time.time() + 3600
|
|
547
580
|
self.__direct_connection_info.remove(conn)
|
|
548
581
|
self.__direct_connection_info.append(conn)
|
|
549
582
|
# None of the direct connections worked, we set a cooldown for direct connections
|
|
550
583
|
self.__direct_connection_consecutive_failures += 1
|
|
551
|
-
self.__direct_connection_cooldown = time.time() + (
|
|
584
|
+
self.__direct_connection_cooldown = time.time() + (
|
|
585
|
+
60 * self.__direct_connection_consecutive_failures
|
|
586
|
+
)
|
|
552
587
|
# 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)
|
|
588
|
+
response = self.myjd.request_api(path, http_action, params, action_url)
|
|
555
589
|
if response is None:
|
|
556
590
|
# My.JDownloader Api failed too.
|
|
557
591
|
return False
|
|
558
592
|
# My.JDownloader Api worked, lets refresh the direct connections and return
|
|
559
593
|
# the response.
|
|
560
594
|
self.__refresh_direct_connections()
|
|
561
|
-
return response[
|
|
595
|
+
return response["data"]
|
|
562
596
|
|
|
563
597
|
def __action_url(self):
|
|
564
598
|
return "/t_" + self.myjd.get_session_token() + "_" + self.device_id
|
|
@@ -569,6 +603,7 @@ class Myjdapi:
|
|
|
569
603
|
Main class for connecting to JD API.
|
|
570
604
|
|
|
571
605
|
"""
|
|
606
|
+
|
|
572
607
|
# Class variable to track connection failures across all instances
|
|
573
608
|
_connection_failed_at = None
|
|
574
609
|
|
|
@@ -616,9 +651,11 @@ class Myjdapi:
|
|
|
616
651
|
|
|
617
652
|
"""
|
|
618
653
|
secret_hash = hashlib.sha256()
|
|
619
|
-
secret_hash.update(
|
|
620
|
-
|
|
621
|
-
|
|
654
|
+
secret_hash.update(
|
|
655
|
+
email.lower().encode("utf-8")
|
|
656
|
+
+ password.encode("utf-8")
|
|
657
|
+
+ domain.lower().encode("utf-8")
|
|
658
|
+
)
|
|
622
659
|
return secret_hash.digest()
|
|
623
660
|
|
|
624
661
|
def __update_encryption_tokens(self):
|
|
@@ -634,8 +671,7 @@ class Myjdapi:
|
|
|
634
671
|
new_token.update(old_token + bytearray.fromhex(self.__session_token))
|
|
635
672
|
self.__server_encryption_token = new_token.digest()
|
|
636
673
|
new_token = hashlib.sha256()
|
|
637
|
-
new_token.update(self.__device_secret +
|
|
638
|
-
bytearray.fromhex(self.__session_token))
|
|
674
|
+
new_token.update(self.__device_secret + bytearray.fromhex(self.__session_token))
|
|
639
675
|
self.__device_encryption_token = new_token.digest()
|
|
640
676
|
|
|
641
677
|
def __signature_create(self, key, data):
|
|
@@ -645,7 +681,7 @@ class Myjdapi:
|
|
|
645
681
|
:param key:
|
|
646
682
|
:param data:
|
|
647
683
|
"""
|
|
648
|
-
signature = hmac.new(key, data.encode(
|
|
684
|
+
signature = hmac.new(key, data.encode("utf-8"), hashlib.sha256)
|
|
649
685
|
return signature.hexdigest()
|
|
650
686
|
|
|
651
687
|
def __decrypt(self, secret_token, data):
|
|
@@ -656,14 +692,12 @@ class Myjdapi:
|
|
|
656
692
|
:param data:
|
|
657
693
|
"""
|
|
658
694
|
init_vector = secret_token[: len(secret_token) // 2]
|
|
659
|
-
key = secret_token[len(secret_token) // 2:]
|
|
695
|
+
key = secret_token[len(secret_token) // 2 :]
|
|
660
696
|
decryptor = AES.new(key, AES.MODE_CBC, init_vector)
|
|
661
697
|
try:
|
|
662
698
|
decrypted_data = unpad(decryptor.decrypt(self.__base64_decode(data)))
|
|
663
699
|
except:
|
|
664
|
-
raise MYJDException(
|
|
665
|
-
"Failed to decode response: {}", data
|
|
666
|
-
)
|
|
700
|
+
raise MYJDException("Failed to decode response: {}", data)
|
|
667
701
|
|
|
668
702
|
return decrypted_data
|
|
669
703
|
|
|
@@ -689,12 +723,12 @@ class Myjdapi:
|
|
|
689
723
|
:param secret_token:
|
|
690
724
|
:param data:
|
|
691
725
|
"""
|
|
692
|
-
data = pad(data.encode(
|
|
693
|
-
init_vector = secret_token[:len(secret_token) // 2]
|
|
694
|
-
key = secret_token[len(secret_token) // 2:]
|
|
726
|
+
data = pad(data.encode("utf-8"))
|
|
727
|
+
init_vector = secret_token[: len(secret_token) // 2]
|
|
728
|
+
key = secret_token[len(secret_token) // 2 :]
|
|
695
729
|
encryptor = AES.new(key, AES.MODE_CBC, init_vector)
|
|
696
730
|
encrypted_data = base64.b64encode(encryptor.encrypt(data))
|
|
697
|
-
return encrypted_data.decode(
|
|
731
|
+
return encrypted_data.decode("utf-8")
|
|
698
732
|
|
|
699
733
|
def update_request_id(self):
|
|
700
734
|
"""
|
|
@@ -731,13 +765,15 @@ class Myjdapi:
|
|
|
731
765
|
|
|
732
766
|
self.__login_secret = self.__secret_create(email, password, "server")
|
|
733
767
|
self.__device_secret = self.__secret_create(email, password, "device")
|
|
734
|
-
response = self.request_api(
|
|
735
|
-
|
|
736
|
-
|
|
768
|
+
response = self.request_api(
|
|
769
|
+
"/my/connect", "GET", [("email", email), ("appkey", self.__app_key)]
|
|
770
|
+
)
|
|
737
771
|
|
|
738
772
|
if response is None:
|
|
739
773
|
# Log and set failure timestamp
|
|
740
|
-
info(
|
|
774
|
+
info(
|
|
775
|
+
"JDownloader API is currently unavailable! Stopping connection attempts for 5 minutes."
|
|
776
|
+
)
|
|
741
777
|
Myjdapi._connection_failed_at = time.time()
|
|
742
778
|
return False
|
|
743
779
|
|
|
@@ -757,8 +793,9 @@ class Myjdapi:
|
|
|
757
793
|
|
|
758
794
|
:returns: boolean -- True if successful, False if there was any error.
|
|
759
795
|
"""
|
|
760
|
-
response = self.request_api(
|
|
761
|
-
|
|
796
|
+
response = self.request_api(
|
|
797
|
+
"/my/listdevices", "GET", [("sessiontoken", self.__session_token)]
|
|
798
|
+
)
|
|
762
799
|
self.update_request_id()
|
|
763
800
|
self.__devices = response["list"]
|
|
764
801
|
|
|
@@ -796,14 +833,16 @@ class Myjdapi:
|
|
|
796
833
|
return Jddevice(self, device)
|
|
797
834
|
raise (MYJDException("Device not found\n"))
|
|
798
835
|
|
|
799
|
-
def request_api(
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
836
|
+
def request_api(
|
|
837
|
+
self,
|
|
838
|
+
path,
|
|
839
|
+
http_method="GET",
|
|
840
|
+
params=None,
|
|
841
|
+
action=None,
|
|
842
|
+
api=None,
|
|
843
|
+
timeout=30,
|
|
844
|
+
output_errors=True,
|
|
845
|
+
):
|
|
807
846
|
"""
|
|
808
847
|
Makes a request to the API to the 'path' using the 'http_method' with parameters,'params'.
|
|
809
848
|
Ex:
|
|
@@ -830,28 +869,39 @@ class Myjdapi:
|
|
|
830
869
|
if self.__server_encryption_token is None:
|
|
831
870
|
query += [
|
|
832
871
|
"signature="
|
|
833
|
-
+ str(
|
|
834
|
-
|
|
872
|
+
+ str(
|
|
873
|
+
self.__signature_create(
|
|
874
|
+
self.__login_secret, query[0] + "&".join(query[1:])
|
|
875
|
+
)
|
|
876
|
+
)
|
|
835
877
|
]
|
|
836
878
|
else:
|
|
837
879
|
query += [
|
|
838
880
|
"signature="
|
|
839
|
-
+ str(
|
|
840
|
-
|
|
881
|
+
+ str(
|
|
882
|
+
self.__signature_create(
|
|
883
|
+
self.__server_encryption_token,
|
|
884
|
+
query[0] + "&".join(query[1:]),
|
|
885
|
+
)
|
|
886
|
+
)
|
|
841
887
|
]
|
|
842
888
|
query = query[0] + "&".join(query[1:])
|
|
843
889
|
|
|
844
|
-
headers = {
|
|
845
|
-
"User-Agent": f"Quasarr/{get_version()}"
|
|
846
|
-
}
|
|
890
|
+
headers = {"User-Agent": f"Quasarr/{get_version()}"}
|
|
847
891
|
try:
|
|
848
|
-
encrypted_response = requests.get(
|
|
892
|
+
encrypted_response = requests.get(
|
|
893
|
+
api + query, timeout=timeout, headers=headers
|
|
894
|
+
)
|
|
849
895
|
except requests.exceptions.ConnectionError:
|
|
850
896
|
return None
|
|
851
897
|
except Exception:
|
|
852
898
|
try:
|
|
853
|
-
encrypted_response = requests.get(
|
|
854
|
-
|
|
899
|
+
encrypted_response = requests.get(
|
|
900
|
+
api + query, timeout=timeout, headers=headers, verify=False
|
|
901
|
+
)
|
|
902
|
+
debug(
|
|
903
|
+
"Could not establish secure connection to JDownloader. Is your time / timezone correct?"
|
|
904
|
+
)
|
|
855
905
|
except requests.exceptions.ConnectionError:
|
|
856
906
|
return None
|
|
857
907
|
except Exception:
|
|
@@ -868,14 +918,13 @@ class Myjdapi:
|
|
|
868
918
|
"apiVer": self.__api_version,
|
|
869
919
|
"url": path,
|
|
870
920
|
"params": params_request,
|
|
871
|
-
"rid": self.__request_id
|
|
921
|
+
"rid": self.__request_id,
|
|
872
922
|
}
|
|
873
923
|
data = json.dumps(params_request)
|
|
874
924
|
# Removing quotes around null elements.
|
|
875
925
|
data = data.replace('"null"', "null")
|
|
876
926
|
data = data.replace("'null'", "null")
|
|
877
|
-
encrypted_data = self.__encrypt(self.__device_encryption_token,
|
|
878
|
-
data)
|
|
927
|
+
encrypted_data = self.__encrypt(self.__device_encryption_token, data)
|
|
879
928
|
if action is not None:
|
|
880
929
|
request_url = api + action + path
|
|
881
930
|
else:
|
|
@@ -885,10 +934,10 @@ class Myjdapi:
|
|
|
885
934
|
request_url,
|
|
886
935
|
headers={
|
|
887
936
|
"Content-Type": "application/aesjson-jd; charset=utf-8",
|
|
888
|
-
"User-Agent": f"Quasarr/{get_version()}"
|
|
937
|
+
"User-Agent": f"Quasarr/{get_version()}",
|
|
889
938
|
},
|
|
890
939
|
data=encrypted_data,
|
|
891
|
-
timeout=timeout
|
|
940
|
+
timeout=timeout,
|
|
892
941
|
)
|
|
893
942
|
except requests.exceptions.ConnectionError:
|
|
894
943
|
return None
|
|
@@ -898,13 +947,15 @@ class Myjdapi:
|
|
|
898
947
|
request_url,
|
|
899
948
|
headers={
|
|
900
949
|
"Content-Type": "application/aesjson-jd; charset=utf-8",
|
|
901
|
-
"User-Agent": f"Quasarr/{get_version()}"
|
|
950
|
+
"User-Agent": f"Quasarr/{get_version()}",
|
|
902
951
|
},
|
|
903
952
|
data=encrypted_data,
|
|
904
953
|
timeout=timeout,
|
|
905
|
-
verify=False
|
|
954
|
+
verify=False,
|
|
955
|
+
)
|
|
956
|
+
debug(
|
|
957
|
+
"Could not establish secure connection to JDownloader. Is your time / timezone correct?"
|
|
906
958
|
)
|
|
907
|
-
debug("Could not establish secure connection to JDownloader. Is your time / timezone correct?")
|
|
908
959
|
except requests.exceptions.ConnectionError:
|
|
909
960
|
return None
|
|
910
961
|
except Exception:
|
|
@@ -918,12 +969,24 @@ class Myjdapi:
|
|
|
918
969
|
error_msg = json.loads(encrypted_response.text)
|
|
919
970
|
except:
|
|
920
971
|
try:
|
|
921
|
-
error_msg = json.loads(
|
|
972
|
+
error_msg = json.loads(
|
|
973
|
+
self.__decrypt(
|
|
974
|
+
self.__device_encryption_token, encrypted_response.text
|
|
975
|
+
)
|
|
976
|
+
)
|
|
922
977
|
except:
|
|
923
|
-
raise MYJDException(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
978
|
+
raise MYJDException(
|
|
979
|
+
"Failed to decode response: {}", encrypted_response.text
|
|
980
|
+
)
|
|
981
|
+
msg = (
|
|
982
|
+
"\n\tSOURCE: "
|
|
983
|
+
+ error_msg["src"]
|
|
984
|
+
+ "\n\tTYPE: "
|
|
985
|
+
+ error_msg["type"]
|
|
986
|
+
+ "\n------\nREQUEST_URL: "
|
|
987
|
+
+ api
|
|
988
|
+
+ path
|
|
989
|
+
)
|
|
927
990
|
if http_method == "GET":
|
|
928
991
|
msg += query
|
|
929
992
|
msg += "\n"
|
|
@@ -932,16 +995,17 @@ class Myjdapi:
|
|
|
932
995
|
raise (MYJDException(msg))
|
|
933
996
|
if action is None:
|
|
934
997
|
if not self.__server_encryption_token:
|
|
935
|
-
response = self.__decrypt(self.__login_secret,
|
|
936
|
-
encrypted_response.text)
|
|
998
|
+
response = self.__decrypt(self.__login_secret, encrypted_response.text)
|
|
937
999
|
else:
|
|
938
|
-
response = self.__decrypt(
|
|
939
|
-
|
|
1000
|
+
response = self.__decrypt(
|
|
1001
|
+
self.__server_encryption_token, encrypted_response.text
|
|
1002
|
+
)
|
|
940
1003
|
else:
|
|
941
|
-
response = self.__decrypt(
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1004
|
+
response = self.__decrypt(
|
|
1005
|
+
self.__device_encryption_token, encrypted_response.text
|
|
1006
|
+
)
|
|
1007
|
+
jsondata = json.loads(response.decode("utf-8"))
|
|
1008
|
+
if jsondata["rid"] != self.__request_id:
|
|
945
1009
|
self.update_request_id()
|
|
946
1010
|
return None
|
|
947
1011
|
self.update_request_id()
|