quasarr 0.1.6__py3-none-any.whl → 1.23.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- quasarr/__init__.py +316 -42
- quasarr/api/__init__.py +187 -0
- quasarr/api/arr/__init__.py +387 -0
- quasarr/api/captcha/__init__.py +1189 -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 +319 -256
- 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 +476 -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/dl.py +199 -0
- quasarr/downloads/sources/dt.py +66 -0
- quasarr/downloads/sources/dw.py +14 -7
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +54 -0
- quasarr/downloads/sources/nx.py +42 -83
- 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/downloads/sources/wx.py +127 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +22 -0
- quasarr/providers/html_templates.py +211 -104
- quasarr/providers/imdb_metadata.py +108 -3
- quasarr/providers/log.py +19 -0
- quasarr/providers/myjd_api.py +201 -40
- quasarr/providers/notifications.py +99 -11
- quasarr/providers/obfuscated.py +65 -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/dl.py +175 -0
- quasarr/providers/sessions/nx.py +76 -0
- quasarr/providers/shared_state.py +656 -79
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +60 -1
- quasarr/providers/web_server.py +1 -1
- quasarr/search/__init__.py +144 -15
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +204 -0
- quasarr/search/sources/dd.py +135 -0
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dl.py +354 -0
- quasarr/search/sources/dt.py +265 -0
- quasarr/search/sources/dw.py +94 -67
- quasarr/search/sources/fx.py +89 -33
- 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 +75 -21
- 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/search/sources/wx.py +337 -0
- quasarr/storage/config.py +39 -10
- quasarr/storage/setup.py +269 -97
- quasarr/storage/sqlite_database.py +6 -1
- quasarr-1.23.0.dist-info/METADATA +306 -0
- quasarr-1.23.0.dist-info/RECORD +77 -0
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/WHEEL +1 -1
- quasarr/arr/__init__.py +0 -423
- quasarr/captcha_solver/__init__.py +0 -284
- quasarr-0.1.6.dist-info/METADATA +0 -81
- quasarr-0.1.6.dist-info/RECORD +0 -31
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/entry_points.txt +0 -0
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info/licenses}/LICENSE +0 -0
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/top_level.txt +0 -0
quasarr/providers/myjd_api.py
CHANGED
|
@@ -35,8 +35,13 @@ import time
|
|
|
35
35
|
from urllib.parse import quote
|
|
36
36
|
|
|
37
37
|
import requests
|
|
38
|
+
import urllib3
|
|
38
39
|
from Cryptodome.Cipher import AES
|
|
39
40
|
|
|
41
|
+
from quasarr.providers.log import info, debug
|
|
42
|
+
from quasarr.providers.version import get_version
|
|
43
|
+
|
|
44
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
40
45
|
BS = 16
|
|
41
46
|
|
|
42
47
|
|
|
@@ -60,6 +65,90 @@ def unpad(s):
|
|
|
60
65
|
return s[0:-s[-1]]
|
|
61
66
|
|
|
62
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
|
+
|
|
63
152
|
class DownloadController:
|
|
64
153
|
"""
|
|
65
154
|
Class that represents the downloads-controller of a Device
|
|
@@ -96,7 +185,7 @@ class Linkgrabber:
|
|
|
96
185
|
self.url = '/linkgrabberv2'
|
|
97
186
|
|
|
98
187
|
def is_collecting(self):
|
|
99
|
-
resp = self.device.action("/
|
|
188
|
+
resp = self.device.action(self.url + "/isCollecting")
|
|
100
189
|
return resp
|
|
101
190
|
|
|
102
191
|
def add_links(self,
|
|
@@ -123,7 +212,33 @@ class Linkgrabber:
|
|
|
123
212
|
"destinationFolder" : null
|
|
124
213
|
}
|
|
125
214
|
"""
|
|
126
|
-
resp = self.device.action("/
|
|
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)
|
|
127
242
|
return resp
|
|
128
243
|
|
|
129
244
|
def remove_links(self, links_ids, packages_ids):
|
|
@@ -208,6 +323,32 @@ class Downloads:
|
|
|
208
323
|
self.device = device
|
|
209
324
|
self.url = "/downloadsV2"
|
|
210
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
|
+
|
|
211
352
|
def query_links(self,
|
|
212
353
|
params=[{
|
|
213
354
|
"bytesTotal": True,
|
|
@@ -258,46 +399,27 @@ class Downloads:
|
|
|
258
399
|
resp = self.device.action(self.url + "/queryPackages", params)
|
|
259
400
|
return resp
|
|
260
401
|
|
|
261
|
-
def remove_links(self, links_ids, packages_ids):
|
|
262
|
-
params = [links_ids, packages_ids]
|
|
263
|
-
resp = self.device.action(self.url + "/removeLinks", params)
|
|
264
|
-
return resp
|
|
265
|
-
|
|
266
402
|
|
|
267
403
|
class Extraction:
|
|
268
404
|
"""
|
|
269
|
-
Class that represents the extraction
|
|
405
|
+
Class that represents the extraction details of a Device
|
|
270
406
|
"""
|
|
271
407
|
|
|
272
408
|
def __init__(self, device):
|
|
273
409
|
self.device = device
|
|
274
|
-
self.url =
|
|
275
|
-
|
|
276
|
-
def get_archive_info(self, link_ids, package_ids):
|
|
277
|
-
params = [link_ids, package_ids]
|
|
278
|
-
resp = self.device.action(self.url + "/getArchiveInfo", params)
|
|
279
|
-
return resp
|
|
410
|
+
self.url = "/extraction"
|
|
280
411
|
|
|
281
|
-
def
|
|
412
|
+
def get_archive_info(self, link_ids=[], package_ids=[]):
|
|
282
413
|
"""
|
|
283
|
-
|
|
414
|
+
Get ArchiveStatus for links and/or packages.
|
|
284
415
|
|
|
285
|
-
:param
|
|
286
|
-
:type
|
|
287
|
-
:param
|
|
288
|
-
:type
|
|
289
|
-
:rtype: boolean indicating success or failure
|
|
416
|
+
:param package_ids: Package UUID's.
|
|
417
|
+
:type: list of strings.
|
|
418
|
+
:param link_ids: link UUID's.
|
|
419
|
+
:type: list of strings
|
|
290
420
|
"""
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"autoExtract": True,
|
|
294
|
-
"removeDownloadLinksAfterExtraction": True,
|
|
295
|
-
"removeFilesAfterExtraction": True
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
params = [archive_id, archive_settings]
|
|
299
|
-
|
|
300
|
-
resp = self.device.action(self.url + "/setArchiveSettings", params)
|
|
421
|
+
params = [link_ids, package_ids]
|
|
422
|
+
resp = self.device.action(self.url + "/getArchiveInfo", params)
|
|
301
423
|
return resp
|
|
302
424
|
|
|
303
425
|
|
|
@@ -316,10 +438,12 @@ class Jddevice:
|
|
|
316
438
|
self.device_id = device_dict["id"]
|
|
317
439
|
self.device_type = device_dict["type"]
|
|
318
440
|
self.myjd = jd
|
|
441
|
+
self.config = Config(self)
|
|
319
442
|
self.linkgrabber = Linkgrabber(self)
|
|
320
443
|
self.downloads = Downloads(self)
|
|
321
|
-
self.downloadcontroller = DownloadController(self)
|
|
322
444
|
self.extraction = Extraction(self)
|
|
445
|
+
self.downloadcontroller = DownloadController(self)
|
|
446
|
+
self.update = Update(self)
|
|
323
447
|
self.__direct_connection_info = None
|
|
324
448
|
self.__refresh_direct_connections()
|
|
325
449
|
self.__direct_connection_enabled = True
|
|
@@ -398,7 +522,7 @@ class Jddevice:
|
|
|
398
522
|
return response['data']
|
|
399
523
|
else:
|
|
400
524
|
# Direct connection info available, we try to use it.
|
|
401
|
-
for conn in self.__direct_connection_info:
|
|
525
|
+
for conn in self.__direct_connection_info[:]:
|
|
402
526
|
connection_ip = conn['conn']['ip']
|
|
403
527
|
# prevent connection to internal docker ip
|
|
404
528
|
if time.time() > conn['cooldown']:
|
|
@@ -445,6 +569,8 @@ class Myjdapi:
|
|
|
445
569
|
Main class for connecting to JD API.
|
|
446
570
|
|
|
447
571
|
"""
|
|
572
|
+
# Class variable to track connection failures across all instances
|
|
573
|
+
_connection_failed_at = None
|
|
448
574
|
|
|
449
575
|
def __init__(self):
|
|
450
576
|
"""
|
|
@@ -453,7 +579,7 @@ class Myjdapi:
|
|
|
453
579
|
"""
|
|
454
580
|
self.__request_id = int(time.time() * 1000)
|
|
455
581
|
self.__api_url = "https://api.jdownloader.org"
|
|
456
|
-
self.__app_key = "
|
|
582
|
+
self.__app_key = "quasarr"
|
|
457
583
|
self.__api_version = 1
|
|
458
584
|
self.__devices = None
|
|
459
585
|
self.__login_secret = None
|
|
@@ -584,6 +710,15 @@ class Myjdapi:
|
|
|
584
710
|
:returns: boolean -- True if succesful, False if there was any error.
|
|
585
711
|
|
|
586
712
|
"""
|
|
713
|
+
# Check if we're in cooldown period (5 minutes = 300 seconds)
|
|
714
|
+
if Myjdapi._connection_failed_at is not None:
|
|
715
|
+
time_since_failure = time.time() - Myjdapi._connection_failed_at
|
|
716
|
+
if time_since_failure < 300:
|
|
717
|
+
# Silently return False during cooldown - don't log anything
|
|
718
|
+
return False
|
|
719
|
+
# Cooldown expired, reset for retry
|
|
720
|
+
Myjdapi._connection_failed_at = None
|
|
721
|
+
|
|
587
722
|
self.update_request_id()
|
|
588
723
|
self.__login_secret = None
|
|
589
724
|
self.__device_secret = None
|
|
@@ -599,6 +734,15 @@ class Myjdapi:
|
|
|
599
734
|
response = self.request_api("/my/connect", "GET", [("email", email),
|
|
600
735
|
("appkey",
|
|
601
736
|
self.__app_key)])
|
|
737
|
+
|
|
738
|
+
if response is None:
|
|
739
|
+
# Log and set failure timestamp
|
|
740
|
+
info("JDownloader API is currently unavailable! Stopping connection attempts for 5 minutes.")
|
|
741
|
+
Myjdapi._connection_failed_at = time.time()
|
|
742
|
+
return False
|
|
743
|
+
|
|
744
|
+
# Connection successful, reset failure timestamp
|
|
745
|
+
Myjdapi._connection_failed_at = None
|
|
602
746
|
self.__connected = True
|
|
603
747
|
self.update_request_id()
|
|
604
748
|
self.__session_token = response["sessiontoken"]
|
|
@@ -696,11 +840,22 @@ class Myjdapi:
|
|
|
696
840
|
query[0] + "&".join(query[1:])))
|
|
697
841
|
]
|
|
698
842
|
query = query[0] + "&".join(query[1:])
|
|
843
|
+
|
|
844
|
+
headers = {
|
|
845
|
+
"User-Agent": f"Quasarr/{get_version()}"
|
|
846
|
+
}
|
|
699
847
|
try:
|
|
700
|
-
encrypted_response = requests.get(api + query, timeout=timeout)
|
|
848
|
+
encrypted_response = requests.get(api + query, timeout=timeout, headers=headers)
|
|
849
|
+
except requests.exceptions.ConnectionError:
|
|
850
|
+
return None
|
|
701
851
|
except Exception:
|
|
702
|
-
|
|
703
|
-
|
|
852
|
+
try:
|
|
853
|
+
encrypted_response = requests.get(api + query, timeout=timeout, headers=headers, verify=False)
|
|
854
|
+
debug("Could not establish secure connection to JDownloader. Is your time / timezone correct?")
|
|
855
|
+
except requests.exceptions.ConnectionError:
|
|
856
|
+
return None
|
|
857
|
+
except Exception:
|
|
858
|
+
return None
|
|
704
859
|
else:
|
|
705
860
|
params_request = []
|
|
706
861
|
if params is not None:
|
|
@@ -729,23 +884,29 @@ class Myjdapi:
|
|
|
729
884
|
encrypted_response = requests.post(
|
|
730
885
|
request_url,
|
|
731
886
|
headers={
|
|
732
|
-
"Content-Type": "application/aesjson-jd; charset=utf-8"
|
|
887
|
+
"Content-Type": "application/aesjson-jd; charset=utf-8",
|
|
888
|
+
"User-Agent": f"Quasarr/{get_version()}"
|
|
733
889
|
},
|
|
734
890
|
data=encrypted_data,
|
|
735
891
|
timeout=timeout
|
|
736
892
|
)
|
|
893
|
+
except requests.exceptions.ConnectionError:
|
|
894
|
+
return None
|
|
737
895
|
except Exception:
|
|
738
896
|
try:
|
|
739
897
|
encrypted_response = requests.post(
|
|
740
898
|
request_url,
|
|
741
899
|
headers={
|
|
742
|
-
"Content-Type": "application/aesjson-jd; charset=utf-8"
|
|
900
|
+
"Content-Type": "application/aesjson-jd; charset=utf-8",
|
|
901
|
+
"User-Agent": f"Quasarr/{get_version()}"
|
|
743
902
|
},
|
|
744
903
|
data=encrypted_data,
|
|
745
904
|
timeout=timeout,
|
|
746
905
|
verify=False
|
|
747
906
|
)
|
|
748
|
-
|
|
907
|
+
debug("Could not establish secure connection to JDownloader. Is your time / timezone correct?")
|
|
908
|
+
except requests.exceptions.ConnectionError:
|
|
909
|
+
return None
|
|
749
910
|
except Exception:
|
|
750
911
|
return None
|
|
751
912
|
if encrypted_response.status_code == 403:
|
|
@@ -3,34 +3,122 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
import requests
|
|
8
9
|
|
|
10
|
+
from quasarr.providers.imdb_metadata import get_imdb_id_from_title
|
|
11
|
+
from quasarr.providers.imdb_metadata import get_poster_link
|
|
12
|
+
from quasarr.providers.log import info
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
# Discord message flag for suppressing notifications
|
|
15
|
+
SUPPRESS_NOTIFICATIONS = 1 << 12 # 4096
|
|
16
|
+
|
|
17
|
+
silent = False
|
|
18
|
+
if os.getenv('SILENT'):
|
|
19
|
+
silent = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def send_discord_message(shared_state, title, case, imdb_id=None, details=None, source=None):
|
|
23
|
+
"""
|
|
24
|
+
Sends a Discord message to the webhook provided in the shared state, based on the specified case.
|
|
25
|
+
|
|
26
|
+
:param shared_state: Shared state object containing configuration.
|
|
27
|
+
:param title: Title of the embed to be sent.
|
|
28
|
+
:param case: A string representing the scenario (e.g., 'captcha', 'failed', 'unprotected').
|
|
29
|
+
:param imdb_id: A string starting with "tt" followed by at least 7 digits, representing an object on IMDb
|
|
30
|
+
:param details: A dictionary containing additional details, such as version and link for updates.
|
|
31
|
+
:param source: Optional source of the notification, sent as a field in the embed.
|
|
32
|
+
:return: True if the message was sent successfully, False otherwise.
|
|
33
|
+
"""
|
|
11
34
|
if not shared_state.values.get("discord"):
|
|
12
35
|
return False
|
|
13
36
|
|
|
37
|
+
poster_object = None
|
|
38
|
+
if case == "unprotected" or case == "captcha":
|
|
39
|
+
if not imdb_id and " " not in title: # this should prevent imdb_search for ebooks and magazines
|
|
40
|
+
imdb_id = get_imdb_id_from_title(shared_state, title)
|
|
41
|
+
if imdb_id:
|
|
42
|
+
poster_link = get_poster_link(shared_state, imdb_id)
|
|
43
|
+
if poster_link:
|
|
44
|
+
poster_object = {
|
|
45
|
+
'url': poster_link
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Decide the embed content based on the case
|
|
49
|
+
if case == "unprotected":
|
|
50
|
+
description = 'No CAPTCHA required. Links were added directly!'
|
|
51
|
+
fields = None
|
|
52
|
+
elif case == "solved":
|
|
53
|
+
description = 'CAPTCHA solved by SponsorsHelper!'
|
|
54
|
+
fields = None
|
|
55
|
+
elif case == "failed":
|
|
56
|
+
description = 'SponsorsHelper failed to solve the CAPTCHA! Package marked es failed.'
|
|
57
|
+
fields = None
|
|
58
|
+
elif case == "captcha":
|
|
59
|
+
description = 'Download will proceed, once the CAPTCHA has been solved.'
|
|
60
|
+
fields = [
|
|
61
|
+
{
|
|
62
|
+
'name': 'Solve CAPTCHA',
|
|
63
|
+
'value': f'Open [this link]({f"{shared_state.values['external_address']}/captcha"}) to solve the CAPTCHA.',
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
if not shared_state.values.get("helper_active"):
|
|
67
|
+
fields.append({
|
|
68
|
+
'name': 'SponsorsHelper',
|
|
69
|
+
'value': f'[Sponsors get automated CAPTCHA solutions!](https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper)',
|
|
70
|
+
})
|
|
71
|
+
elif case == "quasarr_update":
|
|
72
|
+
description = f'Please update to {details["version"]} as soon as possible!'
|
|
73
|
+
if details:
|
|
74
|
+
fields = [
|
|
75
|
+
{
|
|
76
|
+
'name': 'Release notes at: ',
|
|
77
|
+
'value': f'[GitHub.com: rix1337/Quasarr/{details["version"]}]({details["link"]})',
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
else:
|
|
81
|
+
fields = None
|
|
82
|
+
else:
|
|
83
|
+
info(f"Unknown notification case: {case}")
|
|
84
|
+
return False
|
|
85
|
+
|
|
14
86
|
data = {
|
|
15
87
|
'username': 'Quasarr',
|
|
16
|
-
'avatar_url': 'https://
|
|
88
|
+
'avatar_url': 'https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png',
|
|
17
89
|
'embeds': [{
|
|
18
90
|
'title': title,
|
|
19
|
-
'description':
|
|
20
|
-
'fields': [
|
|
21
|
-
{
|
|
22
|
-
'name': '',
|
|
23
|
-
'value': f'[Solve the CAPTCHA here!]({f"{shared_state.values['external_address']}/captcha"})',
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
]
|
|
91
|
+
'description': description,
|
|
27
92
|
}]
|
|
28
93
|
}
|
|
29
94
|
|
|
95
|
+
if source and source.startswith("http"):
|
|
96
|
+
if not fields:
|
|
97
|
+
fields = []
|
|
98
|
+
fields.append({
|
|
99
|
+
'name': 'Source',
|
|
100
|
+
'value': f'[View release details here]({source})',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if fields:
|
|
104
|
+
data['embeds'][0]['fields'] = fields
|
|
105
|
+
|
|
106
|
+
if poster_object:
|
|
107
|
+
data['embeds'][0]['thumbnail'] = poster_object
|
|
108
|
+
data['embeds'][0]['image'] = poster_object
|
|
109
|
+
elif case == "quasarr_update":
|
|
110
|
+
data['embeds'][0]['thumbnail'] = {
|
|
111
|
+
'url': "https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Apply silent mode: suppress notifications for all cases except 'deleted'
|
|
115
|
+
if silent and case not in ["failed", "quasarr_update"]:
|
|
116
|
+
data['flags'] = SUPPRESS_NOTIFICATIONS
|
|
117
|
+
|
|
30
118
|
response = requests.post(shared_state.values["discord"], data=json.dumps(data),
|
|
31
119
|
headers={"Content-Type": "application/json"})
|
|
32
120
|
if response.status_code != 204:
|
|
33
|
-
|
|
121
|
+
info(f"Failed to send message to Discord webhook. Status code: {response.status_code}")
|
|
34
122
|
return False
|
|
35
123
|
|
|
36
124
|
return True
|