quasarr 1.4.1__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 -67
  2. quasarr/api/__init__.py +126 -43
  3. quasarr/api/arr/__init__.py +197 -78
  4. quasarr/api/captcha/__init__.py +885 -39
  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 +236 -487
  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 +48 -39
  30. quasarr/providers/imdb_metadata.py +15 -2
  31. quasarr/providers/myjd_api.py +34 -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 +347 -20
  39. quasarr/providers/statistics.py +154 -0
  40. quasarr/providers/version.py +1 -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 +216 -51
  59. quasarr-1.20.4.dist-info/METADATA +304 -0
  60. quasarr-1.20.4.dist-info/RECORD +72 -0
  61. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
  62. quasarr/providers/tvmaze_metadata.py +0 -23
  63. quasarr-1.4.1.dist-info/METADATA +0 -174
  64. quasarr-1.4.1.dist-info/RECORD +0 -43
  65. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
  66. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
  67. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
@@ -2,14 +2,38 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
+ import quasarr.providers.html_images as images
6
+ from quasarr.providers.version import get_version
7
+
8
+
5
9
  def render_centered_html(inner_content):
6
10
  head = '''
7
11
  <head>
8
12
  <meta charset="utf-8">
9
13
  <meta name="viewport" content="width=device-width, initial-scale=1">
10
14
  <title>Quasarr</title>
11
- <link rel="icon" href="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png">
15
+ <link rel="icon" href="''' + images.logo + '''" type="image/png">
12
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
+ }
13
37
  /* Logo and heading alignment */
14
38
  h1 {
15
39
  display: inline-flex;
@@ -23,7 +47,6 @@ def render_centered_html(inner_content):
23
47
  height: 48px;
24
48
  margin-right: 0.5rem;
25
49
  }
26
-
27
50
  /* Form labels and inputs */
28
51
  label {
29
52
  display: block;
@@ -41,30 +64,10 @@ def render_centered_html(inner_content):
41
64
  color: var(--fg-color);
42
65
  box-sizing: border-box;
43
66
  }
44
-
45
- /* Theme variables */
46
- :root {
47
- --bg-color: #ffffff;
48
- --fg-color: #212529;
49
- --card-bg: #ffffff;
50
- --card-shadow: rgba(0, 0, 0, 0.1);
51
- --primary: #0d6efd;
52
- --secondary: #6c757d;
53
- --code-bg: #f8f9fa;
54
- --spacing: 1rem;
55
- }
56
- @media (prefers-color-scheme: dark) {
57
- :root {
58
- --bg-color: #181a1b;
59
- --fg-color: #f1f1f1;
60
- --card-bg: #242526;
61
- --card-shadow: rgba(0, 0, 0, 0.5);
62
- --code-bg: #2c2f33;
63
- }
64
- }
65
67
  *, *::before, *::after {
66
68
  box-sizing: border-box;
67
69
  }
70
+ /* make body a column flex so footer can stick to bottom */
68
71
  html, body {
69
72
  margin: 0;
70
73
  padding: 0;
@@ -75,12 +78,15 @@ def render_centered_html(inner_content):
75
78
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
76
79
  'Noto Sans', Arial, sans-serif;
77
80
  line-height: 1.6;
81
+ display: flex;
82
+ flex-direction: column;
83
+ min-height: 100vh;
78
84
  }
79
85
  .outer {
86
+ flex: 1;
80
87
  display: flex;
81
88
  justify-content: center;
82
89
  align-items: center;
83
- min-height: 100vh; /* allow container to expand */
84
90
  padding: var(--spacing);
85
91
  }
86
92
  .inner {
@@ -98,7 +104,6 @@ def render_centered_html(inner_content):
98
104
  padding-left: 0;
99
105
  padding-right: 0;
100
106
  }
101
-
102
107
  body:has(iframe) .inner {
103
108
  padding-left: 0;
104
109
  padding-right: 0;
@@ -118,16 +123,10 @@ def render_centered_html(inner_content):
118
123
  p {
119
124
  margin: 0.5rem 0;
120
125
  }
121
- .inline-code, code {
122
- display: block;
126
+ .copy-input {
123
127
  background-color: var(--code-bg);
124
- padding: 0.5rem;
125
- border-radius: 0.5rem;
126
- font-family: monospace;
127
- word-break: break-all;
128
- margin: var(--spacing) 0;
129
128
  }
130
- .api-key-wrapper {
129
+ .url-wrapper .api-key-wrapper {
131
130
  display: flex;
132
131
  gap: 0.5rem;
133
132
  flex-wrap: wrap;
@@ -160,7 +159,14 @@ def render_centered_html(inner_content):
160
159
  text-decoration: none;
161
160
  }
162
161
  a:hover {
163
- text-decoration: underline;
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;
164
170
  }
165
171
  </style>
166
172
  </head>'''
@@ -173,13 +179,15 @@ def render_centered_html(inner_content):
173
179
  {inner_content}
174
180
  </div>
175
181
  </div>
182
+ <footer>
183
+ Quasarr v.{get_version()}
184
+ </footer>
176
185
  </body>
177
186
  '''
178
187
  return f'<html>{body}</html>'
179
188
 
180
189
 
181
190
  def render_button(text, button_type="primary", attributes=None):
182
- # Map types to classes
183
191
  cls = "btn-primary" if button_type == "primary" else "btn-secondary"
184
192
  attr_str = ''
185
193
  if attributes:
@@ -189,7 +197,7 @@ def render_button(text, button_type="primary", attributes=None):
189
197
 
190
198
  def render_form(header, form="", script=""):
191
199
  content = f'''
192
- <h1><img src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
200
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
193
201
  <h2>{header}</h2>
194
202
  {form}
195
203
  {script}
@@ -197,7 +205,7 @@ def render_form(header, form="", script=""):
197
205
  return render_centered_html(content)
198
206
 
199
207
 
200
- def render_success(message, timeout=10):
208
+ def render_success(message, timeout=10, optional_text=""):
201
209
  button_html = render_button(f"Wait time... {timeout}", "secondary", {"id": "nextButton", "disabled": "true"})
202
210
  script = f'''
203
211
  <script>
@@ -216,8 +224,9 @@ def render_success(message, timeout=10):
216
224
  }}, 1000);
217
225
  </script>
218
226
  '''
219
- content = f'''<h1><img src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
227
+ content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
220
228
  <h2>{message}</h2>
229
+ {optional_text}
221
230
  {button_html}
222
231
  {script}
223
232
  '''
@@ -226,7 +235,7 @@ def render_success(message, timeout=10):
226
235
 
227
236
  def render_fail(message):
228
237
  button_html = render_button("Back", "secondary", {"onclick": "window.location.href='/'"})
229
- return render_centered_html(f"""<h1><img src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
238
+ return render_centered_html(f"""<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
230
239
  <h2>{message}</h2>
231
240
  {button_html}
232
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 = []
@@ -829,7 +858,7 @@ class Myjdapi:
829
858
  request_url,
830
859
  headers={
831
860
  "Content-Type": "application/aesjson-jd; charset=utf-8",
832
- "User-Agent": "Quasarr"
861
+ "User-Agent": f"Quasarr/{get_version()}"
833
862
  },
834
863
  data=encrypted_data,
835
864
  timeout=timeout
@@ -840,7 +869,7 @@ class Myjdapi:
840
869
  request_url,
841
870
  headers={
842
871
  "Content-Type": "application/aesjson-jd; charset=utf-8",
843
- "User-Agent": "Quasarr"
872
+ "User-Agent": f"Quasarr/{get_version()}"
844
873
  },
845
874
  data=encrypted_data,
846
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),