quasarr 1.3.5__py3-none-any.whl → 1.20.4__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.

Files changed (67) hide show
  1. quasarr/__init__.py +157 -56
  2. quasarr/api/__init__.py +141 -36
  3. quasarr/api/arr/__init__.py +197 -78
  4. quasarr/api/captcha/__init__.py +897 -42
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +84 -22
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +237 -434
  9. quasarr/downloads/linkcrypters/al.py +237 -0
  10. quasarr/downloads/linkcrypters/filecrypt.py +178 -31
  11. quasarr/downloads/linkcrypters/hide.py +123 -0
  12. quasarr/downloads/packages/__init__.py +461 -0
  13. quasarr/downloads/sources/al.py +697 -0
  14. quasarr/downloads/sources/by.py +106 -0
  15. quasarr/downloads/sources/dd.py +6 -78
  16. quasarr/downloads/sources/dj.py +7 -0
  17. quasarr/downloads/sources/dt.py +1 -1
  18. quasarr/downloads/sources/dw.py +2 -2
  19. quasarr/downloads/sources/he.py +112 -0
  20. quasarr/downloads/sources/mb.py +47 -0
  21. quasarr/downloads/sources/nk.py +51 -0
  22. quasarr/downloads/sources/nx.py +36 -81
  23. quasarr/downloads/sources/sf.py +27 -4
  24. quasarr/downloads/sources/sj.py +7 -0
  25. quasarr/downloads/sources/sl.py +90 -0
  26. quasarr/downloads/sources/wd.py +110 -0
  27. quasarr/providers/cloudflare.py +204 -0
  28. quasarr/providers/html_images.py +20 -0
  29. quasarr/providers/html_templates.py +210 -108
  30. quasarr/providers/imdb_metadata.py +15 -2
  31. quasarr/providers/myjd_api.py +36 -5
  32. quasarr/providers/notifications.py +30 -5
  33. quasarr/providers/obfuscated.py +35 -0
  34. quasarr/providers/sessions/__init__.py +0 -0
  35. quasarr/providers/sessions/al.py +286 -0
  36. quasarr/providers/sessions/dd.py +78 -0
  37. quasarr/providers/sessions/nx.py +76 -0
  38. quasarr/providers/shared_state.py +368 -23
  39. quasarr/providers/statistics.py +154 -0
  40. quasarr/providers/version.py +60 -1
  41. quasarr/search/__init__.py +112 -36
  42. quasarr/search/sources/al.py +448 -0
  43. quasarr/search/sources/by.py +203 -0
  44. quasarr/search/sources/dd.py +17 -6
  45. quasarr/search/sources/dj.py +213 -0
  46. quasarr/search/sources/dt.py +37 -7
  47. quasarr/search/sources/dw.py +27 -47
  48. quasarr/search/sources/fx.py +27 -29
  49. quasarr/search/sources/he.py +196 -0
  50. quasarr/search/sources/mb.py +195 -0
  51. quasarr/search/sources/nk.py +188 -0
  52. quasarr/search/sources/nx.py +22 -6
  53. quasarr/search/sources/sf.py +143 -151
  54. quasarr/search/sources/sj.py +213 -0
  55. quasarr/search/sources/sl.py +246 -0
  56. quasarr/search/sources/wd.py +208 -0
  57. quasarr/storage/config.py +20 -4
  58. quasarr/storage/setup.py +224 -56
  59. quasarr-1.20.4.dist-info/METADATA +304 -0
  60. quasarr-1.20.4.dist-info/RECORD +72 -0
  61. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
  62. quasarr/providers/tvmaze_metadata.py +0 -23
  63. quasarr-1.3.5.dist-info/METADATA +0 -174
  64. quasarr-1.3.5.dist-info/RECORD +0 -43
  65. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
  66. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
  67. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
@@ -2,138 +2,240 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
- def render_centered_html(inner_content):
6
- style_outer = """
7
- display: flex;
8
- justify-content: center;
9
- align-items: center;
10
- height: 100vh;
11
- max-height: 100vh;
12
- overflow-y: auto;
13
- overflow-x: hidden; /* Prevent horizontal scrolling */
14
- width: 100%; /* Ensure it spans full width */
15
- background-color: #212529;
16
- color: #fff;
17
- font-family: system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',
18
- 'Noto Sans','Liberation Sans',Arial,sans-serif,'Apple Color Emoji',
19
- 'Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
20
- """
21
- style_inner = """
22
- background-color: #fff;
23
- border-radius: 0.375rem;
24
- box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
25
- padding: 4px;
26
- text-align: center;
27
- color: #212529;
28
- font-size: 1rem;
29
- font-weight: 400;
30
- line-height: 1.5;
31
- box-sizing: border-box; /* Ensure padding doesn’t exceed boundaries */
32
- max-width: 100%; /* Allow content to shrink */
33
- margin: auto;
34
- """
5
+ import quasarr.providers.html_images as images
6
+ from quasarr.providers.version import get_version
7
+
35
8
 
36
- return f'''
37
- <html>
9
+ def render_centered_html(inner_content):
10
+ head = '''
38
11
  <head>
12
+ <meta charset="utf-8">
39
13
  <meta name="viewport" content="width=device-width, initial-scale=1">
40
14
  <title>Quasarr</title>
41
- </head>
15
+ <link rel="icon" href="''' + images.logo + '''" type="image/png">
16
+ <style>
17
+ /* Theme variables */
18
+ :root {
19
+ --bg-color: #ffffff;
20
+ --fg-color: #212529;
21
+ --card-bg: #ffffff;
22
+ --card-shadow: rgba(0, 0, 0, 0.1);
23
+ --primary: #0d6efd;
24
+ --secondary: #6c757d;
25
+ --code-bg: #f8f9fa;
26
+ --spacing: 1rem;
27
+ }
28
+ @media (prefers-color-scheme: dark) {
29
+ :root {
30
+ --bg-color: #181a1b;
31
+ --fg-color: #f1f1f1;
32
+ --card-bg: #242526;
33
+ --card-shadow: rgba(0, 0, 0, 0.5);
34
+ --code-bg: #2c2f33;
35
+ }
36
+ }
37
+ /* Logo and heading alignment */
38
+ h1 {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ margin-bottom: 0.5rem;
43
+ font-size: 2rem;
44
+ }
45
+ .logo {
46
+ width: 48px;
47
+ height: 48px;
48
+ margin-right: 0.5rem;
49
+ }
50
+ /* Form labels and inputs */
51
+ label {
52
+ display: block;
53
+ font-weight: 600;
54
+ margin-bottom: 0.5rem;
55
+ }
56
+ input, select {
57
+ display: block;
58
+ width: 100%;
59
+ padding: 0.5rem;
60
+ font-size: 1rem;
61
+ border: 1px solid #ced4da;
62
+ border-radius: 0.5rem;
63
+ background-color: var(--card-bg);
64
+ color: var(--fg-color);
65
+ box-sizing: border-box;
66
+ }
67
+ *, *::before, *::after {
68
+ box-sizing: border-box;
69
+ }
70
+ /* make body a column flex so footer can stick to bottom */
71
+ html, body {
72
+ margin: 0;
73
+ padding: 0;
74
+ width: 100%;
75
+ height: 100%;
76
+ background-color: var(--bg-color);
77
+ color: var(--fg-color);
78
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
79
+ 'Noto Sans', Arial, sans-serif;
80
+ line-height: 1.6;
81
+ display: flex;
82
+ flex-direction: column;
83
+ min-height: 100vh;
84
+ }
85
+ .outer {
86
+ flex: 1;
87
+ display: flex;
88
+ justify-content: center;
89
+ align-items: center;
90
+ padding: var(--spacing);
91
+ }
92
+ .inner {
93
+ background-color: var(--card-bg);
94
+ border-radius: 1rem;
95
+ box-shadow: 0 0.5rem 1.5rem var(--card-shadow);
96
+ padding: calc(var(--spacing) * 2);
97
+ text-align: center;
98
+ width: 100%;
99
+ max-width: fit-content;
100
+ }
101
+ /* No padding on the sides for captcha view on small screens */
102
+ @media (max-width: 600px) {
103
+ body:has(iframe) .outer {
104
+ padding-left: 0;
105
+ padding-right: 0;
106
+ }
107
+ body:has(iframe) .inner {
108
+ padding-left: 0;
109
+ padding-right: 0;
110
+ }
111
+ }
112
+ h2 {
113
+ margin-top: var(--spacing);
114
+ margin-bottom: 0.75rem;
115
+ font-size: 1.5rem;
116
+ }
117
+ h3 {
118
+ margin-top: var(--spacing);
119
+ margin-bottom: 0.5rem;
120
+ font-size: 1.125rem;
121
+ font-weight: 500;
122
+ }
123
+ p {
124
+ margin: 0.5rem 0;
125
+ }
126
+ .copy-input {
127
+ background-color: var(--code-bg);
128
+ }
129
+ .url-wrapper .api-key-wrapper {
130
+ display: flex;
131
+ gap: 0.5rem;
132
+ flex-wrap: wrap;
133
+ justify-content: center;
134
+ margin-bottom: var(--spacing);
135
+ }
136
+ .captcha-container {
137
+ background-color: var(--secondary);
138
+ }
139
+ button {
140
+ padding: 0.5rem 1rem;
141
+ font-size: 1rem;
142
+ border-radius: 0.5rem;
143
+ font-weight: 500;
144
+ cursor: pointer;
145
+ transition: background-color 0.2s ease, border-color 0.2s ease;
146
+ border: none;
147
+ margin-top: 0.5rem;
148
+ }
149
+ .btn-primary {
150
+ background-color: var(--primary);
151
+ color: #fff;
152
+ }
153
+ .btn-secondary {
154
+ background-color: var(--secondary);
155
+ color: #fff;
156
+ }
157
+ a {
158
+ color: var(--primary);
159
+ text-decoration: none;
160
+ }
161
+ a:hover {
162
+
163
+ }
164
+ /* footer styling */
165
+ footer {
166
+ text-align: center;
167
+ font-size: 0.75rem;
168
+ color: var(--secondary);
169
+ padding: 0.5rem 0;
170
+ }
171
+ </style>
172
+ </head>'''
173
+
174
+ body = f'''
175
+ {head}
42
176
  <body>
43
- <div style="{style_outer.strip()}">
44
- <div style="{style_inner.strip()}">
45
- {inner_content}
177
+ <div class="outer">
178
+ <div class="inner">
179
+ {inner_content}
180
+ </div>
46
181
  </div>
47
- </div>
182
+ <footer>
183
+ Quasarr v.{get_version()}
184
+ </footer>
48
185
  </body>
49
- </html>
50
186
  '''
187
+ return f'<html>{body}</html>'
51
188
 
52
189
 
53
190
  def render_button(text, button_type="primary", attributes=None):
54
- base_style = (
55
- "padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; "
56
- "border-radius: 0.375rem; color: #fff; display: inline-block; "
57
- "font-weight: 400; text-align: center; vertical-align: middle; "
58
- "cursor: pointer; -webkit-user-select: none; -moz-user-select: none; "
59
- "user-select: none; transition: color 0.15s ease-in-out, "
60
- "background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, "
61
- "box-shadow 0.15s ease-in-out; "
62
- )
63
-
64
- if button_type == "primary":
65
- style = base_style + "background-color: #0d6efd; border: 1px solid #0d6efd; "
66
- else:
67
- style = base_style + "background-color: #6c757d; border: 1px solid #6c757d; "
68
-
69
- attr_str = ' '.join(f'{key}="{value}"' for key, value in attributes.items()) if attributes else ""
70
-
71
- return f'<button style="{style}" {attr_str}>{text}</button>'
191
+ cls = "btn-primary" if button_type == "primary" else "btn-secondary"
192
+ attr_str = ''
193
+ if attributes:
194
+ attr_str = ' '.join(f'{key}="{value}"' for key, value in attributes.items())
195
+ return f'<button class="{cls}" {attr_str}>{text}</button>'
72
196
 
73
197
 
74
198
  def render_form(header, form="", script=""):
75
- styles = """
76
- <style>
77
- input, select {
78
- display: block;
79
- padding: .375rem .75rem;
80
- width: 100%;
81
- font-size: 1rem;
82
- font-weight: 400;
83
- line-height: 1.5;
84
- color: #212529;
85
- background-color: #fff;
86
- border: 1px solid #dee2e6;
87
- border-radius: .375rem;
88
- transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
89
- text-align: center;
90
- margin: 10px auto;
91
- }
92
- </style>
93
- """
94
199
  content = f'''
95
- <h1>Quasarr</h1>
96
- <h3>{header}</h3>
97
- {styles}
200
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
201
+ <h2>{header}</h2>
98
202
  {form}
99
203
  {script}
100
204
  '''
101
205
  return render_centered_html(content)
102
206
 
103
207
 
104
- def render_success(message, timeout=10):
208
+ def render_success(message, timeout=10, optional_text=""):
105
209
  button_html = render_button(f"Wait time... {timeout}", "secondary", {"id": "nextButton", "disabled": "true"})
106
- script = f"""
107
- <script>
108
- var counter = {timeout};
109
- var interval = setInterval(function() {{
110
- counter--;
111
- document.getElementById('nextButton').innerText = 'Wait time... ' + counter;
112
- if (counter === 0) {{
113
- clearInterval(interval);
114
- document.getElementById('nextButton').innerText = 'Continue';
115
- document.getElementById('nextButton').disabled = false;
116
- document.getElementById('nextButton').onclick = function() {{
117
- window.location.href='/';
118
- }};
119
- // Change button style to primary
120
- document.getElementById('nextButton').style.backgroundColor = '#0d6efd';
121
- document.getElementById('nextButton').style.borderColor = '#0d6efd';
122
- }}
123
- }}, 1000);
124
- </script>
125
- """
126
- content = f"""<h1>Quasarr</h1>
127
- <h3>{message}</h3>
128
- {button_html}
129
- {script}
130
- """
210
+ script = f'''
211
+ <script>
212
+ let counter = {timeout};
213
+ const btn = document.getElementById('nextButton');
214
+ const interval = setInterval(() => {{
215
+ counter--;
216
+ btn.innerText = `Wait time... ${{counter}}`;
217
+ if (counter === 0) {{
218
+ clearInterval(interval);
219
+ btn.innerText = 'Continue';
220
+ btn.disabled = false;
221
+ btn.className = 'btn-primary';
222
+ btn.onclick = () => window.location.href = '/';
223
+ }}
224
+ }}, 1000);
225
+ </script>
226
+ '''
227
+ content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
228
+ <h2>{message}</h2>
229
+ {optional_text}
230
+ {button_html}
231
+ {script}
232
+ '''
131
233
  return render_centered_html(content)
132
234
 
133
235
 
134
236
  def render_fail(message):
135
237
  button_html = render_button("Back", "secondary", {"onclick": "window.location.href='/'"})
136
- return render_centered_html(f"""<h1>Quasarr</h1>
137
- <h3>{message}</h3>
138
- {button_html}
139
- """)
238
+ return render_centered_html(f"""<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
239
+ <h2>{message}</h2>
240
+ {button_html}
241
+ """)
@@ -2,6 +2,7 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
+ import html
5
6
  import re
6
7
  from datetime import datetime, timedelta
7
8
  from json import loads
@@ -61,6 +62,11 @@ def get_localized_title(shared_state, imdb_id, language='de'):
61
62
  if not localized_title:
62
63
  debug(f"Could not get localized title for {imdb_id} in {language} from IMDb")
63
64
 
65
+ localized_title = html.unescape(localized_title)
66
+ localized_title = re.sub(r"[^a-zA-Z0-9äöüÄÖÜß&-']", ' ', localized_title).strip()
67
+ localized_title = localized_title.replace(" - ", "-")
68
+ localized_title = re.sub(r'\s{2,}', ' ', localized_title)
69
+
64
70
  return localized_title
65
71
 
66
72
 
@@ -111,8 +117,15 @@ def get_imdb_id_from_title(shared_state, title, language="de"):
111
117
 
112
118
  if len(search_results) > 0:
113
119
  for result in search_results:
114
- if shared_state.search_string_in_sanitized_title(title, f"{result['titleNameText']}"):
115
- imdb_id = result['id']
120
+ try:
121
+ found_title = result["listItem"]["titleText"]
122
+ found_id = result["listItem"]["titleId"]
123
+ except KeyError:
124
+ found_title = result["titleNameText"]
125
+ found_id = result['id']
126
+
127
+ if shared_state.search_string_in_sanitized_title(title, found_title):
128
+ imdb_id = found_id
116
129
  break
117
130
  else:
118
131
  debug(f"Request on IMDb failed: {results.status_code}")
@@ -39,6 +39,7 @@ import urllib3
39
39
  from Cryptodome.Cipher import AES
40
40
 
41
41
  from quasarr.providers.log import debug
42
+ from quasarr.providers.version import get_version
42
43
 
43
44
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
44
45
  BS = 16
@@ -399,6 +400,29 @@ class Downloads:
399
400
  return resp
400
401
 
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
+
402
426
  class Jddevice:
403
427
  """
404
428
  Class that represents a JDownloader device and it's functions
@@ -417,6 +441,7 @@ class Jddevice:
417
441
  self.config = Config(self)
418
442
  self.linkgrabber = Linkgrabber(self)
419
443
  self.downloads = Downloads(self)
444
+ self.extraction = Extraction(self)
420
445
  self.downloadcontroller = DownloadController(self)
421
446
  self.update = Update(self)
422
447
  self.__direct_connection_info = None
@@ -552,7 +577,7 @@ class Myjdapi:
552
577
  """
553
578
  self.__request_id = int(time.time() * 1000)
554
579
  self.__api_url = "https://api.jdownloader.org"
555
- self.__app_key = "http://git.io/vmcsk"
580
+ self.__app_key = "quasarr"
556
581
  self.__api_version = 1
557
582
  self.__devices = None
558
583
  self.__login_secret = None
@@ -795,10 +820,14 @@ class Myjdapi:
795
820
  query[0] + "&".join(query[1:])))
796
821
  ]
797
822
  query = query[0] + "&".join(query[1:])
823
+
824
+ headers = {
825
+ "User-Agent": f"Quasarr/{get_version()}"
826
+ }
798
827
  try:
799
- encrypted_response = requests.get(api + query, timeout=timeout)
828
+ encrypted_response = requests.get(api + query, timeout=timeout, headers=headers)
800
829
  except Exception:
801
- encrypted_response = requests.get(api + query, timeout=timeout, verify=False)
830
+ encrypted_response = requests.get(api + query, timeout=timeout, headers=headers, verify=False)
802
831
  debug("Could not establish secure connection to JDownloader.")
803
832
  else:
804
833
  params_request = []
@@ -828,7 +857,8 @@ class Myjdapi:
828
857
  encrypted_response = requests.post(
829
858
  request_url,
830
859
  headers={
831
- "Content-Type": "application/aesjson-jd; charset=utf-8"
860
+ "Content-Type": "application/aesjson-jd; charset=utf-8",
861
+ "User-Agent": f"Quasarr/{get_version()}"
832
862
  },
833
863
  data=encrypted_data,
834
864
  timeout=timeout
@@ -838,7 +868,8 @@ class Myjdapi:
838
868
  encrypted_response = requests.post(
839
869
  request_url,
840
870
  headers={
841
- "Content-Type": "application/aesjson-jd; charset=utf-8"
871
+ "Content-Type": "application/aesjson-jd; charset=utf-8",
872
+ "User-Agent": f"Quasarr/{get_version()}"
842
873
  },
843
874
  data=encrypted_data,
844
875
  timeout=timeout,
@@ -19,7 +19,7 @@ if os.getenv('SILENT'):
19
19
  silent = True
20
20
 
21
21
 
22
- def send_discord_message(shared_state, title, case, imdb_id=None):
22
+ def send_discord_message(shared_state, title, case, imdb_id=None, details=None, source=None):
23
23
  """
24
24
  Sends a Discord message to the webhook provided in the shared state, based on the specified case.
25
25
 
@@ -27,6 +27,8 @@ def send_discord_message(shared_state, title, case, imdb_id=None):
27
27
  :param title: Title of the embed to be sent.
28
28
  :param case: A string representing the scenario (e.g., 'captcha', 'failed', 'unprotected').
29
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.
30
32
  :return: True if the message was sent successfully, False otherwise.
31
33
  """
32
34
  if not shared_state.values.get("discord"):
@@ -34,7 +36,7 @@ def send_discord_message(shared_state, title, case, imdb_id=None):
34
36
 
35
37
  poster_object = None
36
38
  if case == "unprotected" or case == "captcha":
37
- if not imdb_id:
39
+ if not imdb_id and " " not in title: # this should prevent imdb_search for ebooks and magazines
38
40
  imdb_id = get_imdb_id_from_title(shared_state, title)
39
41
  if imdb_id:
40
42
  poster_link = get_poster_link(shared_state, imdb_id)
@@ -64,8 +66,19 @@ def send_discord_message(shared_state, title, case, imdb_id=None):
64
66
  if not shared_state.values.get("helper_active"):
65
67
  fields.append({
66
68
  'name': 'SponsorsHelper',
67
- 'value': f'[Become a Sponsor and let SponsorsHelper solve CAPTCHAs for you!]({f"https://github.com/users/rix1337/sponsorship"})',
68
- }, )
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
69
82
  else:
70
83
  info(f"Unknown notification case: {case}")
71
84
  return False
@@ -79,15 +92,27 @@ def send_discord_message(shared_state, title, case, imdb_id=None):
79
92
  }]
80
93
  }
81
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
+
82
103
  if fields:
83
104
  data['embeds'][0]['fields'] = fields
84
105
 
85
106
  if poster_object:
86
107
  data['embeds'][0]['thumbnail'] = poster_object
87
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
+ }
88
113
 
89
114
  # Apply silent mode: suppress notifications for all cases except 'deleted'
90
- if silent and case != "failed":
115
+ if silent and case not in ["failed", "quasarr_update"]:
91
116
  data['flags'] = SUPPRESS_NOTIFICATIONS
92
117
 
93
118
  response = requests.post(shared_state.values["discord"], data=json.dumps(data),