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
quasarr/storage/config.py CHANGED
@@ -26,12 +26,28 @@ class Config(object):
26
26
  ("device", "str", ""),
27
27
  ],
28
28
  'Hostnames': [
29
+ ("al", "secret", ""),
30
+ ("by", "secret", ""),
29
31
  ("dd", "secret", ""),
32
+ ("dj", "secret", ""),
30
33
  ("dt", "secret", ""),
31
34
  ("dw", "secret", ""),
32
35
  ("fx", "secret", ""),
36
+ ("he", "secret", ""),
37
+ ("mb", "secret", ""),
38
+ ("nk", "secret", ""),
33
39
  ("nx", "secret", ""),
34
- ("sf", "secret", "")
40
+ ("sf", "secret", ""),
41
+ ("sj", "secret", ""),
42
+ ("sl", "secret", ""),
43
+ ("wd", "secret", "")
44
+ ],
45
+ 'FlareSolverr': [
46
+ ("url", "str", ""),
47
+ ],
48
+ 'AL': [
49
+ ("user", "secret", ""),
50
+ ("password", "secret", "")
35
51
  ],
36
52
  'DD': [
37
53
  ("user", "secret", ""),
@@ -54,10 +70,10 @@ class Config(object):
54
70
  self._section) or self._set_default_config(self._section)
55
71
  self.__config__ = self._read_config(self._section)
56
72
  except configparser.DuplicateSectionError:
57
- print('Doppelte Sektion in der Konfigurationsdatei.')
73
+ print('Duplicate Section in Config File')
58
74
  raise
59
- except:
60
- print('Ein unbekannter Fehler in der Konfigurationsdatei ist aufgetreten.')
75
+ except Exception as e:
76
+ print(f'Unknown error while reading config file: {e}')
61
77
  raise
62
78
 
63
79
  def _set_default_config(self, section):
quasarr/storage/setup.py CHANGED
@@ -5,12 +5,14 @@
5
5
  import os
6
6
  import sys
7
7
 
8
+ import requests
8
9
  from bottle import Bottle, request
9
10
 
10
11
  import quasarr
11
- import quasarr.downloads.sources.dd
12
- from quasarr.downloads.sources import dd
13
- from quasarr.downloads.sources import nx
12
+ import quasarr.providers.html_images as images
13
+ import quasarr.providers.sessions.al
14
+ import quasarr.providers.sessions.dd
15
+ import quasarr.providers.sessions.nx
14
16
  from quasarr.providers.html_templates import render_button, render_form, render_success, render_fail
15
17
  from quasarr.providers.log import info
16
18
  from quasarr.providers.shared_state import extract_valid_hostname
@@ -65,54 +67,157 @@ def path_config(shared_state):
65
67
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
66
68
 
67
69
 
70
+ def hostname_form_html(shared_state, message):
71
+ hostname_fields = '''
72
+ <label for="{id}" style="display:inline-flex; align-items:center; gap:4px;">{label}{img_html}</label>
73
+ <input type="text" id="{id}" name="{id}" placeholder="example.com" autocorrect="off" autocomplete="off" value="{value}"><br>
74
+ '''
75
+
76
+ field_html = []
77
+ hostnames = Config('Hostnames') # Load once outside the loop
78
+ for label in shared_state.values["sites"]:
79
+ field_id = label.lower()
80
+ img_html = ''
81
+ try:
82
+ img_data = getattr(images, field_id)
83
+ if img_data:
84
+ img_html = f' <img src="{img_data}" width="16" height="16" style="filter: blur(2px);" alt="{label} icon">'
85
+ except AttributeError:
86
+ pass
87
+
88
+ # Get the current value (if any and non-empty)
89
+ current_value = hostnames.get(field_id)
90
+ if not current_value:
91
+ current_value = '' # Ensure it's empty if None or ""
92
+
93
+ field_html.append(hostname_fields.format(
94
+ id=field_id,
95
+ label=label,
96
+ img_html=img_html,
97
+ value=current_value
98
+ ))
99
+
100
+ hostname_form_content = "".join(field_html)
101
+ button_html = render_button("Save", "primary", {"type": "submit"})
102
+
103
+ template = """
104
+ <div id="message" style="margin-bottom:0.5em;">{message}</div>
105
+ <div id="error-msg" style="color:red; margin-bottom:1em;"></div>
106
+
107
+ <form action="/api/hostnames" method="post" onsubmit="return validateHostnames(this)">
108
+ {hostname_form_content}
109
+ {button}
110
+ </form>
111
+
112
+ <script>
113
+ function validateHostnames(form) {{
114
+ var errorDiv = document.getElementById('error-msg');
115
+ errorDiv.textContent = '';
116
+
117
+ var inputs = form.querySelectorAll('input[type="text"]');
118
+ for (var i = 0; i < inputs.length; i++) {{
119
+ if (inputs[i].value.trim() !== '') {{
120
+ return true;
121
+ }}
122
+ }}
123
+
124
+ errorDiv.textContent = 'Please fill in at least one hostname!';
125
+ inputs[0].focus();
126
+ return false;
127
+ }}
128
+ </script>
129
+ """
130
+ return template.format(
131
+ message=message,
132
+ hostname_form_content=hostname_form_content,
133
+ button=button_html
134
+ )
135
+
136
+
137
+ def save_hostnames(shared_state, timeout=5, first_run=True):
138
+ hostnames = Config('Hostnames')
139
+
140
+ # Collect submitted hostnames, validate, and track errors
141
+ valid_domains = {}
142
+ errors = {}
143
+
144
+ for site_key in shared_state.values['sites']:
145
+ shorthand = site_key.lower()
146
+ raw_value = request.forms.get(shorthand)
147
+ # treat missing or empty string as intentional clear, no validation
148
+ if raw_value is None or raw_value.strip() == '':
149
+ continue
150
+
151
+ # non-empty submission: must validate
152
+ result = extract_valid_hostname(raw_value, shorthand)
153
+ domain = result.get('domain')
154
+ message = result.get('message', 'Error checking the hostname you provided!')
155
+ if domain:
156
+ valid_domains[site_key] = domain
157
+ else:
158
+ errors[site_key] = message
159
+
160
+ # Filter out any accidental empty domains and require at least one valid hostname overall
161
+ valid_domains = {k: d for k, d in valid_domains.items() if d}
162
+ if not valid_domains:
163
+ # report last or generic message
164
+ fail_msg = next(iter(errors.values()), 'No valid hostname provided!')
165
+ return render_fail(fail_msg)
166
+
167
+ # Save: valid ones, explicit empty for those omitted cleanly, leave untouched if error
168
+ changed_sites = []
169
+ for site_key in shared_state.values['sites']:
170
+ shorthand = site_key.lower()
171
+ raw_value = request.forms.get(shorthand)
172
+ # determine if change applies
173
+ if site_key in valid_domains:
174
+ new_val = valid_domains[site_key]
175
+ old_val = hostnames.get(shorthand) or ''
176
+ if old_val != new_val:
177
+ hostnames.save(shorthand, new_val)
178
+ changed_sites.append(shorthand)
179
+ elif raw_value is None:
180
+ # no submission: leave untouched
181
+ continue
182
+ elif raw_value.strip() == '':
183
+ old_val = hostnames.get(shorthand) or ''
184
+ if old_val != '':
185
+ hostnames.save(shorthand, '')
186
+
187
+ quasarr.providers.web_server.temp_server_success = True
188
+
189
+ # Build success message, include any per-site errors
190
+ success_msg = 'At least one valid hostname set!'
191
+ if errors:
192
+ optional_text = "<br>".join(f"{site}: {msg}" for site, msg in errors.items()) + "<br>"
193
+ else:
194
+ optional_text = "All provided hostnames are valid.<br>"
195
+
196
+ if not first_run:
197
+ # Append restart notice for specific sites that actually changed
198
+ for site in changed_sites:
199
+ if site.lower() in {'al', 'dd', 'nx'}:
200
+ optional_text += f"{site.upper()}: You must restart Quasarr and follow additional steps to start using this site.<br>"
201
+
202
+ return render_success(success_msg, timeout, optional_text=optional_text)
203
+
204
+
68
205
  def hostnames_config(shared_state):
69
206
  app = Bottle()
70
207
 
71
208
  @app.get('/')
72
209
  def hostname_form():
73
- hostname_fields = '''
74
- <label for="{id}">{label}</label>
75
- <input type="text" id="{id}" name="{id}" placeholder="example.com" autocorrect="off" autocomplete="off"><br>
76
- '''
77
-
78
- hostname_form_content = "".join(
79
- [hostname_fields.format(id=label.lower(), label=label) for label in shared_state.values["sites"]])
80
-
81
- hostname_form_html = f'''
82
- <form action="/api/hostnames" method="post">
83
- {hostname_form_content}
84
- {render_button("Save", "primary", {"type": "submit"})}
85
- </form>
86
- '''
87
-
88
- return render_form("Set at least one valid hostname", hostname_form_html)
210
+ message = """<p>
211
+ If you're having trouble setting this up, take a closer look at
212
+ <a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank" rel="noopener noreferrer">
213
+ step one of these instructions.
214
+ </a>
215
+ </p>"""
216
+ return render_form("Set at least one valid hostname", hostname_form_html(shared_state, message))
89
217
 
90
218
  @app.post("/api/hostnames")
91
219
  def set_hostnames():
92
- hostnames = Config('Hostnames')
93
-
94
- hostname_set = False
95
-
96
- for key in shared_state.values["sites"]:
97
- shorthand = key.lower()
98
- hostname = request.forms.get(shorthand)
99
- try:
100
- if hostname:
101
- hostname = extract_valid_hostname(hostname, shorthand)
102
- except Exception as e:
103
- info(f"Error extracting domain from {hostname}: {e}")
104
- continue
105
-
106
- if hostname:
107
- hostnames.save(key, hostname)
108
- hostname_set = True
109
-
110
- if hostname_set:
111
- quasarr.providers.web_server.temp_server_success = True
112
- return render_success("At least one valid hostname set",
113
- 5)
114
- else:
115
- return render_fail("No valid hostname provided!")
220
+ return save_hostnames(shared_state)
116
221
 
117
222
  info(f'Hostnames not set. Starting web server for config at: "{shared_state.values['internal_address']}".')
118
223
  info("Please set at least one valid hostname there!")
@@ -145,7 +250,7 @@ def hostname_credentials_config(shared_state, shorthand, domain):
145
250
  return render_form(f"Set User and Password for {shorthand}", form_html)
146
251
 
147
252
  @app.post("/api/credentials/<sh>")
148
- def set_nx_credentials(sh):
253
+ def set_credentials(sh):
149
254
  user = request.forms.get('user')
150
255
  password = request.forms.get('password')
151
256
  config = Config(shorthand)
@@ -154,12 +259,16 @@ def hostname_credentials_config(shared_state, shorthand, domain):
154
259
  config.save("user", user)
155
260
  config.save("password", password)
156
261
 
262
+ if sh.lower() == "al":
263
+ if quasarr.providers.sessions.al.create_and_persist_session(shared_state):
264
+ quasarr.providers.web_server.temp_server_success = True
265
+ return render_success(f"{sh} credentials set successfully", 5)
157
266
  if sh.lower() == "dd":
158
- if dd.create_and_persist_session(shared_state):
267
+ if quasarr.providers.sessions.dd.create_and_persist_session(shared_state):
159
268
  quasarr.providers.web_server.temp_server_success = True
160
269
  return render_success(f"{sh} credentials set successfully", 5)
161
270
  if sh.lower() == "nx":
162
- if nx.create_and_persist_session(shared_state):
271
+ if quasarr.providers.sessions.nx.create_and_persist_session(shared_state):
163
272
  quasarr.providers.web_server.temp_server_success = True
164
273
  return render_success(f"{sh} credentials set successfully", 5)
165
274
 
@@ -175,15 +284,70 @@ def hostname_credentials_config(shared_state, shorthand, domain):
175
284
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
176
285
 
177
286
 
287
+ def flaresolverr_config(shared_state):
288
+ app = Bottle()
289
+
290
+ @app.get('/')
291
+ def url_form():
292
+ form_content = '''
293
+ <span><a href="https://github.com/FlareSolverr/FlareSolverr?tab=readme-ov-file#installation">A local instance</a>
294
+ must be running and reachable to Quasarr!</span><br><br>
295
+ <label for="url">FlareSolverr URL</label>
296
+ <input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1"><br>
297
+ '''
298
+ form_html = f'''
299
+ <form action="/api/flaresolverr" method="post">
300
+ {form_content}
301
+ {render_button("Save", "primary", {"type": "submit"})}
302
+ </form>
303
+ '''
304
+ return render_form("Set FlareSolverr URL", form_html)
305
+
306
+ @app.post('/api/flaresolverr')
307
+ def set_flaresolverr_url():
308
+ url = request.forms.get('url').strip()
309
+ config = Config("FlareSolverr")
310
+
311
+ if url:
312
+ try:
313
+ headers = {"Content-Type": "application/json"}
314
+ data = {
315
+ "cmd": "request.get",
316
+ "url": "http://www.google.com/",
317
+ "maxTimeout": 30000
318
+ }
319
+ response = requests.post(url, headers=headers, json=data, timeout=30)
320
+ if response.status_code == 200:
321
+ config.save("url", url)
322
+ print(f'Using Flaresolverr URL: "{url}"')
323
+ quasarr.providers.web_server.temp_server_success = True
324
+ return render_success("FlareSolverr URL saved successfully!", 5)
325
+ except requests.RequestException:
326
+ pass
327
+
328
+ # on failure, clear any existing value and notify user
329
+ config.save("url", "")
330
+ return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
331
+
332
+ info(
333
+ '"flaresolverr" URL is required for proper operation. '
334
+ f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
335
+ )
336
+ info("Please enter your FlareSolverr URL now.")
337
+ return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
338
+
339
+
178
340
  def jdownloader_config(shared_state):
179
341
  app = Bottle()
180
342
 
181
343
  @app.get('/')
182
- def hostname_form():
344
+ def jd_form():
183
345
  verify_form_html = f'''
184
346
  <span>If required register account at: <a href="https://my.jdownloader.org/login.html#register">
185
- my.jdownloader.org</a>!</span><br><br>
186
-
347
+ my.jdownloader.org</a>!</span><br>
348
+
349
+ <p><strong>JDownloader must be running and connected to My JDownloader!</strong></p><br>
350
+
187
351
  <form id="verifyForm" action="/api/verify_jdownloader" method="post">
188
352
  <label for="user">E-Mail</label>
189
353
  <input type="text" id="user" name="user" placeholder="user@example.org" autocorrect="off"><br>
@@ -193,9 +357,9 @@ def jdownloader_config(shared_state):
193
357
  "secondary",
194
358
  {"id": "verifyButton", "type": "button", "onclick": "verifyCredentials()"})}
195
359
  </form>
196
-
360
+
197
361
  <p>Some JDownloader settings will be enforced by Quasarr on startup.</p>
198
-
362
+
199
363
  <form action="/api/store_jdownloader" method="post" id="deviceForm" style="display: none;">
200
364
  <input type="hidden" id="hiddenUser" name="user">
201
365
  <input type="hidden" id="hiddenPass" name="pass">
@@ -203,6 +367,7 @@ def jdownloader_config(shared_state):
203
367
  <select id="device" name="device"></select><br>
204
368
  {render_button("Save", "primary", {"type": "submit"})}
205
369
  </form>
370
+ <p><strong>Saving may take a while!</strong></p><br>
206
371
  '''
207
372
 
208
373
  verify_script = '''
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.4
2
+ Name: quasarr
3
+ Version: 1.20.4
4
+ Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
5
+ Home-page: https://github.com/rix1337/Quasarr
6
+ Author: rix1337
7
+ Author-email:
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: beautifulsoup4>=4.14.2
14
+ Requires-Dist: bottle>=0.13.4
15
+ Requires-Dist: dukpy>=0.5.0
16
+ Requires-Dist: pillow>=12.0.0
17
+ Requires-Dist: pycryptodomex>=3.23.0
18
+ Requires-Dist: requests>=2.32.5
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+ Dynamic: summary
27
+
28
+ #
29
+
30
+ <img src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" data-canonical-src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" width="64" height="64" />
31
+
32
+ Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
33
+
34
+ [![PyPI version](https://badge.fury.io/py/quasarr.svg)](https://badge.fury.io/py/quasarr)
35
+ [![Discord](https://img.shields.io/discord/1075348594225315891)](https://discord.gg/eM4zA2wWQb)
36
+ [![GitHub Sponsorship](https://img.shields.io/badge/support-me-red.svg)](https://github.com/users/rix1337/sponsorship)
37
+
38
+ Quasarr pretends to be both `Newznab Indexer` and `SABnzbd client`. Therefore, do not try to use it with real usenet
39
+ indexers or download clients. It simply does not know what NZB or torrent files are.
40
+
41
+ Quasarr includes a solution to quickly and easily decrypt protected links.
42
+ [Active monthly Sponsors get access to SponsorsHelper to do so automatically.](https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper)
43
+ Alternatively, follow the link from the console output (or discord notification) to solve CAPTCHAs manually.
44
+ Quasarr will confidently handle the rest.
45
+
46
+ # Instructions
47
+ 1. Set up and run [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) 3.4.4 or later.
48
+ 2. Set up and run [JDownloader 2](https://jdownloader.org/download/index).
49
+ 3. Follow the next steps.
50
+
51
+ ---
52
+
53
+ ## FlareSolverr
54
+ 1. Ensure your running FlareSolverr is reachable by Quasarr.
55
+ 2. Provide your FlareSolverr URL to Quasarr during the setup process.
56
+ 3. The full URL must include the version path, e.g., `http://192.168.1.1:8191/v1`.
57
+
58
+ ---
59
+
60
+ ## Quasarr
61
+
62
+ Tell Quasarr which sites to search for releases. It requires at least one valid source to start up.
63
+
64
+ > - By default, Quasarr does **not** know which sites to scrape for download links.
65
+ > - The setup will guide you through the process of providing valid hostnames for Quasarr to scrape.
66
+ > - Do **not** ask for help here if you do not know which hostnames to use. Picking them is solely your responsibility.
67
+ > - You may check sites like [Pastebin](https://pastebin.com/search?q=hostnames+quasarr) for user‑submitted suggestions.
68
+
69
+ ---
70
+
71
+ ## JDownloader
72
+
73
+ 1. Ensure your running JDownloader is connected to the My JDownloader service.
74
+ 2. Provide your [My‑JDownloader‑Credentials](https://my.jdownloader.org) to Quasarr during the setup process.
75
+
76
+ > - Consider setting up a fresh JDownloader before you begin.
77
+ > - JDownloader must be running and available to Quasarr.
78
+ > - Quasarr will modify JDownloader’s settings so downloads can be handled by Radarr/Sonarr/LazyLibrarian.
79
+ > - If using Docker, ensure that JDownloader’s download path is available to Radarr/Sonarr/LazyLibrarian with **exactly the same** internal and external path mapping (matching only the external path is not enough).
80
+
81
+ ---
82
+
83
+ ## Radarr / Sonarr
84
+
85
+ Set up Quasarr as a **Newznab Indexer** and **SABnzbd Download Client**:
86
+
87
+ 1. **URL**: Use the `URL` from the **API Information** section of the console output (or copy it from the Quasarr web UI).
88
+ 2. **API Key**: Use the `API Key` from the **API Information** section of the console output (or copy it from the Quasarr web UI).
89
+ 3. Leave all other settings at their defaults.
90
+
91
+ > **Important notice for Sonarr**
92
+ > - Ensure all shows (including anime) are set to the **Standard** series type.
93
+ > - Quasarr will never find releases for shows set to **Anime / Absolute**.
94
+
95
+ ---
96
+
97
+ ## LazyLibrarian
98
+
99
+ > **Important notice**
100
+ > - This feature is experimental and may not work as expected.
101
+ > - Quasarr cannot help you with metadata issues, missing covers, or other LazyLibrarian problems.
102
+ > - Please report issues when one of your hostnames yields results through their website, but not in LazyLibrarian.
103
+
104
+ Set up Quasarr as a **SABnzbd+ Downloader**
105
+
106
+ 1. **SABnzbd URL/Port**: Use port and host parts from `URL` found in the **API Information** section of the console output (or copy it from the Quasarr web UI).
107
+ 2. **SABnzbd API Key**: Use the `API Key` from the **API Information** section of the console output (or copy it from the Quasarr web UI).
108
+ 3. **SABnzbd Category**: Use `docs` to ensure LazyLibrarian does not interfere with Radarr/Sonarr.
109
+ 4. Press `Test SABnzbd` to verify the connection, then `Save changes`.
110
+
111
+ Set up Quasarr as a **Newznab Provider**:
112
+ 1. **Newznab URL**: Use the `URL` from the **API Information** section of the console output (or copy it from the Quasarr web UI).
113
+ 2. **Newznab API** Use the `API Key` from the **API Information** section of the console output (or copy it from the Quasarr web UI).
114
+ 3. Press `Test` to verify the connection, then `Save changes`.
115
+
116
+ Fix the `Importing` settings:
117
+ 1. Check `Enable OpenLibrary api for book/author information`
118
+ 2. Select `OpenLibrary` below `Primary Information Source`
119
+ 2. Under `Import languages` add `, Unknown` (and for German users: `, de, ger, de-DE`).
120
+
121
+ Fix the `Processing` settings:
122
+ 1. Under `Folders` add the full Quasarr download path, typically `/downloads/Quasarr/`
123
+ 2. If you do not do this, processing after the download will fail.
124
+
125
+
126
+
127
+ ---
128
+
129
+ ## Advanced Settings
130
+
131
+ To restrict results to a specific mirror, add the mirror name to the Newznab/indexer URL.
132
+ > **Example:** Appending `/api/dropbox/` will only return releases where `dropbox` is explicitly mentioned in a link.
133
+ > **Caution:** If a mirror is not available at a hostname, the release will be ignored or the download will fail. Use this option carefully.
134
+
135
+ To see download status information in Radarr/Sonarr
136
+ 1. Open `Activity` → `Queue` → `Options`
137
+ 2. Enable `Release Title`
138
+
139
+ # Docker
140
+
141
+ It is highly recommended to run the latest docker image with all optional variables set.
142
+
143
+ ```
144
+ docker run -d \
145
+ --name="Quasarr" \
146
+ -p port:8080 \
147
+ -v /path/to/config/:/config:rw \
148
+ -e 'INTERNAL_ADDRESS'='http://192.168.0.1:8080' \
149
+ -e 'EXTERNAL_ADDRESS'='https://foo.bar/' \
150
+ -e 'DISCORD'='https://discord.com/api/webhooks/1234567890/ABCDEFGHIJKLMN' \
151
+ -e 'HOSTNAMES'='https://pastebin.com/raw/eX4Mpl3'
152
+ -e 'SILENT'='True' \
153
+ -e 'DEBUG'='' \
154
+ ghcr.io/rix1337/quasarr:latest
155
+ ```
156
+
157
+ * `INTERNAL_ADDRESS` is required so Radarr/Sonarr/LazyLibrarian can reach Quasarr. **Must** include port!
158
+ * `EXTERNAL_ADDRESS` is optional and helpful if using a reverse proxy. Always protect external access with basic auth!
159
+ * `DISCORD` is optional and must be a valid Discord webhook URL.
160
+ * `HOSTNAMES` is optional and allows skipping the manual hostname step during setup.
161
+ * Must be a publicly available `HTTP` or `HTTPs` link
162
+ * Must be a raw `.ini` / text file (not HTML or JSON)
163
+ * Must contain at least one valid Hostname per line `ab = xyz`
164
+ * `SILENT` is optional and silences all discord notifications except for error messages from SponsorsHelper if `True`.
165
+ * `DEBUG` is optional and enables debug logging if `True`.
166
+
167
+ # Manual setup
168
+
169
+ Use this only in case you can't run the docker image.
170
+
171
+ `pip install quasarr`
172
+
173
+ * Requires Python 3.12 or later
174
+ * Requires [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr)
175
+ * Requires [JDownloader 2](https://jdownloader.org/download/index) with [My JDownloader](https://my.jdownloader.org/)
176
+
177
+ ```
178
+ --port=8080
179
+ --discord=https://discord.com/api/webhooks/1234567890/ABCDEFGHIJKLMN
180
+ --external_address=https://foo.bar/
181
+ --hostnames=https://pastebin.com/raw/eX4Mpl3
182
+ ```
183
+
184
+ * `--discord` see `DISCORD`docker variable
185
+ * `--external_address` see `EXTERNAL_ADDRESS`docker variable
186
+ * `--hostnames` see `HOSTNAMES`docker variable
187
+
188
+ # Philosophy
189
+
190
+ Complexity is the killer of small projects like this one. It must be fought at all cost!
191
+
192
+ We will not waste precious time on features that will slow future development cycles down.
193
+ Most feature requests can be satisfied by:
194
+
195
+ - Existing settings in Radarr/Sonarr/LazyLibrarian
196
+ - Existing settings in JDownloader
197
+ - Existing tools from the *arr ecosystem that integrate directly with Radarr/Sonarr/LazyLibrarian
198
+
199
+ # Roadmap
200
+
201
+ - Assume there are zero known
202
+ issues [unless you find one or more open issues in this repository](https://github.com/rix1337/Quasarr/issues).
203
+ - Still having an issue? Provide a detailed report [here](https://github.com/rix1337/Quasarr/issues/new/choose)!
204
+ - There are no hostname integrations in active development unless you see an open pull request
205
+ [here](https://github.com/rix1337/Quasarr/pulls).
206
+ - Pull requests are welcome. Especially for popular hostnames.
207
+ - Always reach out on Discord before starting work on a new feature to prevent waste of time.
208
+ - Please follow the existing code style and project structure.
209
+ - Anti-bot measures must be circumvented fully by Quasarr. Thus you will need to provide a working solution for new
210
+ CAPTCHA types by integrating it in the Quasarr Web UI.
211
+ - Please provide proof of functionality (screenshots/examples) when submitting your pull request.
212
+
213
+ # SponsorsHelper
214
+
215
+ <img src="https://imgur.com/iHBqLwT.png" width="64" height="64" />
216
+
217
+ SponsorsHelper is a Docker image that solves CAPTCHAs and decrypts links for Quasarr.
218
+ Image access is limited to [active monthly GitHub sponsors](https://github.com/users/rix1337/sponsorship).
219
+
220
+ [![Github Sponsorship](https://img.shields.io/badge/support-me-red.svg)](https://github.com/users/rix1337/sponsorship)
221
+
222
+ ---
223
+
224
+ ## 🔑 GitHub Token Setup
225
+
226
+ 1. Start your [sponsorship](https://github.com/users/rix1337/sponsorship) first.
227
+ 2. Open [GitHub Classic Token Settings](https://github.com/settings/tokens/new?type=classic)
228
+ 3. Name it (e.g., `SponsorsHelper`) and choose unlimited expiration
229
+ 4. Enable these scopes:
230
+ - `read:packages`
231
+ - `read:user`
232
+ - `read:org`
233
+ 5. Click **Generate token** and copy it for the next steps
234
+
235
+ ---
236
+
237
+ ## 🐋 Docker Login
238
+
239
+ ```bash
240
+ echo "GITHUB_TOKEN" | docker login ghcr.io -u USERNAME --password-stdin
241
+ ````
242
+
243
+ * `USERNAME` → your GitHub username
244
+ * `GITHUB_TOKEN` → the token you just created
245
+
246
+ ---
247
+
248
+ ⚠️ **Before logging in, the image will not download.**
249
+
250
+ ---
251
+
252
+ ## ▶️ Run SponsorsHelper
253
+
254
+ ```bash
255
+ docker run -d \
256
+ --name='SponsorsHelper' \
257
+ -e 'QUASARR_URL'='http://192.168.0.1:8080' \
258
+ -e 'DEATHBYCAPTCHA_TOKEN'='2FMum5zuDBxMmbXDIsADnllEFl73bomydIpzo7...' \
259
+ -e 'GITHUB_TOKEN'='ghp_123.....456789' \
260
+ -e 'FLARESOLVERR_URL'='http://10.10.0.1:8191/v1' \
261
+ -e 'NX_USER'='your_nx_username' \
262
+ -e 'NX_PASS'='your_nx_password' \
263
+ -e 'JUNKIES_USER'='your_junkies_username' \
264
+ -e 'JUNKIES_PASS'='your_junkies_password' \
265
+ -e 'JUNKIES_HOSTER'='your_desired_hoster' \
266
+ ghcr.io/rix1337-sponsors/docker/helper:latest
267
+ ```
268
+
269
+ ### Required Parameters
270
+
271
+ * `QUASARR_URL` → Local URL of Quasarr
272
+ * `DEATHBYCAPTCHA_TOKEN` → [DeathByCaptcha](https://deathbycaptcha.com/register?refid=6184288242b) account token
273
+ * `GITHUB_TOKEN` → Classic GitHub PAT with the scopes listed above
274
+ * `FLARESOLVERR_URL` → Local URL of [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) - required!
275
+ * `NX_USER` / `NX_PASS` → NX account credentials
276
+ * `JUNKIES_USER` / `JUNKIES_PASS` → Junkies account credentials
277
+ * `JUNKIES_HOSTER` → Preferred hoster for Junkies links
278
+ ---
279
+
280
+ ⚠️ **Without a valid GitHub token linked to an active sponsorship, the image will not run.**
281
+
282
+ ---
283
+
284
+ # Development Setup for Pull Requests
285
+
286
+ To test your changes before submitting a pull request:
287
+
288
+ **Run Quasarr with the `--internal_address` parameter:**
289
+
290
+ ```bash
291
+ python Quasarr.py --internal_address=http://<host-ip>:<port>
292
+ ```
293
+
294
+ Replace `<host-ip>` and `<port>` with the scheme, IP, and port of your host machine.
295
+ The `--internal_address` parameter is **mandatory**.
296
+
297
+ **Start the required services using the `dev-services-compose.yml` file:**
298
+
299
+ ```bash
300
+ CONFIG_VOLUMES=/path/to/config docker-compose -f docker/dev-services-compose.yml up
301
+ ```
302
+
303
+ Replace `/path/to/config` with your desired configuration location.
304
+ The `CONFIG_VOLUMES` environment variable is **mandatory**.