quasarr 2.4.8__py3-none-any.whl → 2.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. quasarr/__init__.py +134 -70
  2. quasarr/api/__init__.py +40 -31
  3. quasarr/api/arr/__init__.py +116 -108
  4. quasarr/api/captcha/__init__.py +262 -137
  5. quasarr/api/config/__init__.py +76 -46
  6. quasarr/api/packages/__init__.py +138 -102
  7. quasarr/api/sponsors_helper/__init__.py +29 -16
  8. quasarr/api/statistics/__init__.py +19 -19
  9. quasarr/downloads/__init__.py +165 -72
  10. quasarr/downloads/linkcrypters/al.py +35 -18
  11. quasarr/downloads/linkcrypters/filecrypt.py +107 -52
  12. quasarr/downloads/linkcrypters/hide.py +5 -6
  13. quasarr/downloads/packages/__init__.py +342 -177
  14. quasarr/downloads/sources/al.py +191 -100
  15. quasarr/downloads/sources/by.py +31 -13
  16. quasarr/downloads/sources/dd.py +27 -14
  17. quasarr/downloads/sources/dj.py +1 -3
  18. quasarr/downloads/sources/dl.py +126 -71
  19. quasarr/downloads/sources/dt.py +11 -5
  20. quasarr/downloads/sources/dw.py +28 -14
  21. quasarr/downloads/sources/he.py +32 -24
  22. quasarr/downloads/sources/mb.py +19 -9
  23. quasarr/downloads/sources/nk.py +14 -10
  24. quasarr/downloads/sources/nx.py +8 -18
  25. quasarr/downloads/sources/sf.py +45 -20
  26. quasarr/downloads/sources/sj.py +1 -3
  27. quasarr/downloads/sources/sl.py +9 -5
  28. quasarr/downloads/sources/wd.py +32 -12
  29. quasarr/downloads/sources/wx.py +35 -21
  30. quasarr/providers/auth.py +42 -37
  31. quasarr/providers/cloudflare.py +28 -30
  32. quasarr/providers/hostname_issues.py +2 -1
  33. quasarr/providers/html_images.py +2 -2
  34. quasarr/providers/html_templates.py +22 -14
  35. quasarr/providers/imdb_metadata.py +149 -80
  36. quasarr/providers/jd_cache.py +131 -39
  37. quasarr/providers/log.py +1 -1
  38. quasarr/providers/myjd_api.py +260 -196
  39. quasarr/providers/notifications.py +53 -41
  40. quasarr/providers/obfuscated.py +9 -4
  41. quasarr/providers/sessions/al.py +71 -55
  42. quasarr/providers/sessions/dd.py +21 -14
  43. quasarr/providers/sessions/dl.py +30 -19
  44. quasarr/providers/sessions/nx.py +23 -14
  45. quasarr/providers/shared_state.py +292 -141
  46. quasarr/providers/statistics.py +75 -43
  47. quasarr/providers/utils.py +33 -27
  48. quasarr/providers/version.py +45 -14
  49. quasarr/providers/web_server.py +10 -5
  50. quasarr/search/__init__.py +30 -18
  51. quasarr/search/sources/al.py +124 -73
  52. quasarr/search/sources/by.py +110 -59
  53. quasarr/search/sources/dd.py +57 -35
  54. quasarr/search/sources/dj.py +69 -48
  55. quasarr/search/sources/dl.py +159 -100
  56. quasarr/search/sources/dt.py +110 -74
  57. quasarr/search/sources/dw.py +121 -61
  58. quasarr/search/sources/fx.py +108 -62
  59. quasarr/search/sources/he.py +78 -49
  60. quasarr/search/sources/mb.py +96 -48
  61. quasarr/search/sources/nk.py +80 -50
  62. quasarr/search/sources/nx.py +91 -62
  63. quasarr/search/sources/sf.py +171 -106
  64. quasarr/search/sources/sj.py +69 -48
  65. quasarr/search/sources/sl.py +115 -71
  66. quasarr/search/sources/wd.py +67 -44
  67. quasarr/search/sources/wx.py +188 -123
  68. quasarr/storage/config.py +65 -52
  69. quasarr/storage/setup.py +238 -140
  70. quasarr/storage/sqlite_database.py +10 -4
  71. {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
  72. quasarr-2.4.9.dist-info/RECORD +81 -0
  73. quasarr-2.4.8.dist-info/RECORD +0 -81
  74. {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
  75. {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
  76. {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/storage/setup.py CHANGED
@@ -15,13 +15,22 @@ import quasarr.providers.sessions.al
15
15
  import quasarr.providers.sessions.dd
16
16
  import quasarr.providers.sessions.dl
17
17
  import quasarr.providers.sessions.nx
18
- from quasarr.providers.auth import add_auth_routes, add_auth_hook
18
+ from quasarr.providers.auth import add_auth_hook, add_auth_routes
19
19
  from quasarr.providers.hostname_issues import get_all_hostname_issues
20
- from quasarr.providers.html_templates import render_button, render_form, render_success, render_fail, \
21
- render_centered_html
20
+ from quasarr.providers.html_templates import (
21
+ render_button,
22
+ render_centered_html,
23
+ render_fail,
24
+ render_form,
25
+ render_success,
26
+ )
22
27
  from quasarr.providers.log import info
23
28
  from quasarr.providers.shared_state import extract_valid_hostname
24
- from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys, FALLBACK_USER_AGENT
29
+ from quasarr.providers.utils import (
30
+ FALLBACK_USER_AGENT,
31
+ extract_allowed_keys,
32
+ extract_kv_pairs,
33
+ )
25
34
  from quasarr.providers.web_server import Server
26
35
  from quasarr.storage.config import Config
27
36
  from quasarr.storage.sqlite_database import DataBase
@@ -29,10 +38,13 @@ from quasarr.storage.sqlite_database import DataBase
29
38
 
30
39
  def render_reconnect_success(message, countdown_seconds=3):
31
40
  """Render a success page that waits, then polls until the server is back online."""
32
- button_html = render_button(f"Continuing in {countdown_seconds}...", "secondary",
33
- {"id": "reconnectBtn", "disabled": "true"})
41
+ button_html = render_button(
42
+ f"Continuing in {countdown_seconds}...",
43
+ "secondary",
44
+ {"id": "reconnectBtn", "disabled": "true"},
45
+ )
34
46
 
35
- script = f'''
47
+ script = f"""
36
48
  <script>
37
49
  var remaining = {countdown_seconds};
38
50
  var btn = document.getElementById('reconnectBtn');
@@ -72,7 +84,7 @@ def render_reconnect_success(message, countdown_seconds=3):
72
84
  attempt();
73
85
  }}
74
86
  </script>
75
- '''
87
+ """
76
88
 
77
89
  content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
78
90
  <h2>✅ Success</h2>
@@ -86,11 +98,11 @@ def render_reconnect_success(message, countdown_seconds=3):
86
98
  def add_no_cache_headers(app):
87
99
  """Add hooks to prevent browser caching of setup pages."""
88
100
 
89
- @app.hook('after_request')
101
+ @app.hook("after_request")
90
102
  def set_no_cache():
91
- response.set_header('Cache-Control', 'no-cache, no-store, must-revalidate')
92
- response.set_header('Pragma', 'no-cache')
93
- response.set_header('Expires', '0')
103
+ response.set_header("Cache-Control", "no-cache, no-store, must-revalidate")
104
+ response.set_header("Pragma", "no-cache")
105
+ response.set_header("Expires", "0")
94
106
 
95
107
 
96
108
  def setup_auth(app):
@@ -106,7 +118,7 @@ def path_config(shared_state):
106
118
 
107
119
  current_path = os.path.dirname(os.path.abspath(sys.argv[0]))
108
120
 
109
- @app.get('/')
121
+ @app.get("/")
110
122
  def config_form():
111
123
  config_form_html = f'''
112
124
  <form action="/api/config" method="post" onsubmit="return handleSubmit(this)">
@@ -125,8 +137,9 @@ def path_config(shared_state):
125
137
  }}
126
138
  </script>
127
139
  '''
128
- return render_form("Press 'Save' to set desired path for configuration",
129
- config_form_html)
140
+ return render_form(
141
+ "Press 'Save' to set desired path for configuration", config_form_html
142
+ )
130
143
 
131
144
  def set_config_path(config_path):
132
145
  config_path_file = "Quasarr.conf"
@@ -135,7 +148,7 @@ def path_config(shared_state):
135
148
  config_path = current_path
136
149
 
137
150
  config_path = config_path.replace("\\", "/")
138
- config_path = config_path[:-1] if config_path.endswith('/') else config_path
151
+ config_path = config_path[:-1] if config_path.endswith("/") else config_path
139
152
 
140
153
  if not os.path.exists(config_path):
141
154
  os.makedirs(config_path)
@@ -152,42 +165,54 @@ def path_config(shared_state):
152
165
  quasarr.providers.web_server.temp_server_success = True
153
166
  return render_reconnect_success(f'Config path set to: "{config_path}"')
154
167
 
155
- info(f'Starting web server for config at: "{shared_state.values['internal_address']}".')
168
+ info(
169
+ f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
170
+ )
156
171
  info("Please set desired config path there!")
157
172
  quasarr.providers.web_server.temp_server_success = False
158
- return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
173
+ return Server(
174
+ app, listen="0.0.0.0", port=shared_state.values["port"]
175
+ ).serve_temporarily()
159
176
 
160
177
 
161
178
  def _escape_js_for_html_attr(s):
162
179
  """Escape a string for use inside a JS string literal within an HTML attribute."""
163
180
  if s is None:
164
181
  return ""
165
- return str(s).replace("\\", "\\\\").replace("'", "\\'").replace('"', '&quot;').replace("\n", "\\n").replace("\r",
166
- "")
182
+ return (
183
+ str(s)
184
+ .replace("\\", "\\\\")
185
+ .replace("'", "\\'")
186
+ .replace('"', "&quot;")
187
+ .replace("\n", "\\n")
188
+ .replace("\r", "")
189
+ )
167
190
 
168
191
 
169
- def hostname_form_html(shared_state, message, show_restart_button=False, show_skip_management=False):
170
- hostname_fields = '''
192
+ def hostname_form_html(
193
+ shared_state, message, show_restart_button=False, show_skip_management=False
194
+ ):
195
+ hostname_fields = """
171
196
  <label for="{id}" onclick="showStatusDetail(\'{id}\', \'{label}\', \'{status}\', \'{error_details_for_modal}\', \'{timestamp}\', \'{operation}\', \'{url}\')"
172
197
  style="cursor:pointer; display:inline-flex; align-items:center; gap:4px;" title="{status_title}">
173
198
  <span class="status-indicator" id="status-{id}" data-status="{status}">{status_emoji}</span>
174
199
  {label}
175
200
  </label>
176
201
  <input type="text" id="{id}" name="{id}" placeholder="example.com" autocorrect="off" autocomplete="off" value="{value}"><br>
177
- '''
202
+ """
178
203
 
179
- skip_indicator = '''
204
+ skip_indicator = """
180
205
  <div class="skip-indicator" id="skip-indicator-{id}" style="margin-top:-0.5rem; margin-bottom:0.75rem; padding:0.5rem; background:var(--code-bg, #f8f9fa); border-radius:0.25rem; font-size:0.875rem;">
181
206
  <span style="color:#dc3545;">⚠️ Login skipped</span>
182
207
  <button type="button" class="btn-subtle" style="margin-left:0.5rem; padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="clearSkipLogin('{id}', this)">Clear &amp; require login</button>
183
208
  </div>
184
- '''
209
+ """
185
210
 
186
211
  field_html = []
187
- hostnames = Config('Hostnames') # Load once outside the loop
212
+ hostnames = Config("Hostnames") # Load once outside the loop
188
213
  skip_login_db = DataBase("skip_login")
189
214
  hostname_issues = get_all_hostname_issues()
190
- login_required_sites = ['al', 'dd', 'dl', 'nx']
215
+ login_required_sites = ["al", "dd", "dl", "nx"]
191
216
 
192
217
  for label in shared_state.values["sites"]:
193
218
  field_id = label.lower()
@@ -195,14 +220,18 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
195
220
  # Get the current value (if any and non-empty)
196
221
  current_value = hostnames.get(field_id)
197
222
  if not current_value:
198
- current_value = '' # Ensure it's empty if None or ""
223
+ current_value = "" # Ensure it's empty if None or ""
199
224
 
200
225
  # Determine traffic light status
201
- is_login_skipped = field_id in login_required_sites and skip_login_db.retrieve(field_id)
226
+ is_login_skipped = field_id in login_required_sites and skip_login_db.retrieve(
227
+ field_id
228
+ )
202
229
  issue = hostname_issues.get(field_id)
203
230
  timestamp = ""
204
231
  operation = ""
205
- error_details_for_modal = "" # New variable to hold the full error message for the modal
232
+ error_details_for_modal = (
233
+ "" # New variable to hold the full error message for the modal
234
+ )
206
235
 
207
236
  if not current_value:
208
237
  status = "unset"
@@ -218,7 +247,9 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
218
247
  status = "error"
219
248
  status_emoji = "🔴"
220
249
  operation = issue.get("operation", "unknown")
221
- error_details_for_modal = issue.get("error", "Unknown error") # Get the full error message
250
+ error_details_for_modal = issue.get(
251
+ "error", "Unknown error"
252
+ ) # Get the full error message
222
253
  timestamp = issue.get("timestamp", "")
223
254
  status_title = f"Error in {operation}"
224
255
  else:
@@ -227,18 +258,22 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
227
258
  status_title = "Working normally"
228
259
  error_details_for_modal = "Configured and working normally."
229
260
 
230
- field_html.append(hostname_fields.format(
231
- id=field_id,
232
- label=_escape_js_for_html_attr(label),
233
- value=current_value,
234
- status=status,
235
- status_emoji=status_emoji,
236
- status_title=status_title,
237
- error_details_for_modal=_escape_js_for_html_attr(error_details_for_modal),
238
- timestamp=timestamp,
239
- operation=_escape_js_for_html_attr(operation),
240
- url=_escape_js_for_html_attr(current_value)
241
- ))
261
+ field_html.append(
262
+ hostname_fields.format(
263
+ id=field_id,
264
+ label=_escape_js_for_html_attr(label),
265
+ value=current_value,
266
+ status=status,
267
+ status_emoji=status_emoji,
268
+ status_title=status_title,
269
+ error_details_for_modal=_escape_js_for_html_attr(
270
+ error_details_for_modal
271
+ ),
272
+ timestamp=timestamp,
273
+ operation=_escape_js_for_html_attr(operation),
274
+ url=_escape_js_for_html_attr(current_value),
275
+ )
276
+ )
242
277
 
243
278
  # Add skip indicator for login-required sites if skip management is enabled
244
279
  if show_skip_management and field_id in login_required_sites:
@@ -246,20 +281,22 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
246
281
  field_html.append(skip_indicator.format(id=field_id))
247
282
 
248
283
  hostname_form_content = "".join(field_html)
249
- button_html = render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})
284
+ button_html = render_button(
285
+ "Save", "primary", {"type": "submit", "id": "submitBtn"}
286
+ )
250
287
 
251
288
  # Get stored hostnames URL if available
252
- stored_url = Config('Settings').get("hostnames_url") or ""
289
+ stored_url = Config("Settings").get("hostnames_url") or ""
253
290
 
254
291
  # Build restart button HTML if needed
255
292
  restart_section = ""
256
293
  if show_restart_button:
257
- restart_section = f'''
294
+ restart_section = f"""
258
295
  <div class="section-divider" style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--divider-color, #dee2e6);">
259
296
  <p style="font-size:0.875rem; color:var(--secondary, #6c757d);">Restart required after changing login-required hostnames (AL, DD, DL, NX)</p>
260
297
  {render_button("Restart Quasarr", "secondary", {"type": "button", "onclick": "confirmRestart()"})}
261
298
  </div>
262
- '''
299
+ """
263
300
 
264
301
  template = """
265
302
  <style>
@@ -600,28 +637,28 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
600
637
  hostname_form_content=hostname_form_content,
601
638
  button=button_html,
602
639
  stored_url=stored_url,
603
- restart_section=restart_section
640
+ restart_section=restart_section,
604
641
  )
605
642
 
606
643
 
607
644
  def save_hostnames(shared_state, timeout=5, first_run=True):
608
- hostnames = Config('Hostnames')
645
+ hostnames = Config("Hostnames")
609
646
 
610
647
  # Collect submitted hostnames, validate, and track errors
611
648
  valid_domains = {}
612
649
  errors = {}
613
650
 
614
- for site_key in shared_state.values['sites']:
651
+ for site_key in shared_state.values["sites"]:
615
652
  shorthand = site_key.lower()
616
653
  raw_value = request.forms.get(shorthand)
617
654
  # treat missing or empty string as intentional clear, no validation
618
- if raw_value is None or raw_value.strip() == '':
655
+ if raw_value is None or raw_value.strip() == "":
619
656
  continue
620
657
 
621
658
  # non-empty submission: must validate
622
659
  result = extract_valid_hostname(raw_value, shorthand)
623
- domain = result.get('domain')
624
- message = result.get('message', 'Error checking the hostname you provided!')
660
+ domain = result.get("domain")
661
+ message = result.get("message", "Error checking the hostname you provided!")
625
662
  if domain:
626
663
  valid_domains[site_key] = domain
627
664
  else:
@@ -631,47 +668,49 @@ def save_hostnames(shared_state, timeout=5, first_run=True):
631
668
  valid_domains = {k: d for k, d in valid_domains.items() if d}
632
669
  if not valid_domains:
633
670
  # report last or generic message
634
- fail_msg = next(iter(errors.values()), 'No valid hostname provided!')
671
+ fail_msg = next(iter(errors.values()), "No valid hostname provided!")
635
672
  return render_fail(fail_msg)
636
673
 
637
674
  # Save: valid ones, explicit empty for those omitted cleanly, leave untouched if error
638
675
  changed_sites = []
639
- for site_key in shared_state.values['sites']:
676
+ for site_key in shared_state.values["sites"]:
640
677
  shorthand = site_key.lower()
641
678
  raw_value = request.forms.get(shorthand)
642
679
  # determine if change applies
643
680
  if site_key in valid_domains:
644
681
  new_val = valid_domains[site_key]
645
- old_val = hostnames.get(shorthand) or ''
682
+ old_val = hostnames.get(shorthand) or ""
646
683
  if old_val != new_val:
647
684
  hostnames.save(shorthand, new_val)
648
685
  changed_sites.append(shorthand)
649
686
  elif raw_value is None:
650
687
  # no submission: leave untouched
651
688
  continue
652
- elif raw_value.strip() == '':
653
- old_val = hostnames.get(shorthand) or ''
654
- if old_val != '':
655
- hostnames.save(shorthand, '')
689
+ elif raw_value.strip() == "":
690
+ old_val = hostnames.get(shorthand) or ""
691
+ if old_val != "":
692
+ hostnames.save(shorthand, "")
656
693
 
657
694
  # Handle hostnames URL storage
658
- hostnames_url = request.forms.get('hostnames_url', '').strip()
695
+ hostnames_url = request.forms.get("hostnames_url", "").strip()
659
696
  settings_config = Config("Settings")
660
697
  settings_config.save("hostnames_url", hostnames_url)
661
698
 
662
699
  quasarr.providers.web_server.temp_server_success = True
663
700
 
664
701
  # Build success message, include any per-site errors
665
- success_msg = 'At least one valid hostname set!'
702
+ success_msg = "At least one valid hostname set!"
666
703
  if errors:
667
- optional_text = "<br>".join(f"{site}: {msg}" for site, msg in errors.items()) + "<br>"
704
+ optional_text = (
705
+ "<br>".join(f"{site}: {msg}" for site, msg in errors.items()) + "<br>"
706
+ )
668
707
  else:
669
708
  optional_text = "All provided hostnames are valid.<br>"
670
709
 
671
710
  if not first_run:
672
711
  # Append restart notice for specific sites that actually changed
673
712
  for site in changed_sites:
674
- if site.lower() in {'al', 'dd', 'dl', 'nx'}:
713
+ if site.lower() in {"al", "dd", "dl", "nx"}:
675
714
  optional_text += f"{site.upper()}: You must restart Quasarr and follow additional steps to start using this site.<br>"
676
715
 
677
716
  full_message = f"{success_msg}<br><small>{optional_text}</small>"
@@ -683,7 +722,7 @@ def hostnames_config(shared_state):
683
722
  add_no_cache_headers(app)
684
723
  setup_auth(app)
685
724
 
686
- @app.get('/')
725
+ @app.get("/")
687
726
  def hostname_form():
688
727
  message = """<p>
689
728
  If you're having trouble setting this up, take a closer look at
@@ -691,7 +730,9 @@ def hostnames_config(shared_state):
691
730
  the instructions.
692
731
  </a>
693
732
  </p>"""
694
- return render_form("Set at least one valid hostname", hostname_form_html(shared_state, message))
733
+ return render_form(
734
+ "Set at least one valid hostname", hostname_form_html(shared_state, message)
735
+ )
695
736
 
696
737
  @app.post("/api/hostnames")
697
738
  def set_hostnames():
@@ -700,10 +741,10 @@ def hostnames_config(shared_state):
700
741
  @app.post("/api/hostnames/import-url")
701
742
  def import_hostnames_from_url():
702
743
  """Fetch URL and parse hostnames, return JSON for JS to populate fields."""
703
- response.content_type = 'application/json'
744
+ response.content_type = "application/json"
704
745
  try:
705
746
  data = request.json
706
- url = data.get('url', '').strip()
747
+ url = data.get("url", "").strip()
707
748
 
708
749
  if not url:
709
750
  return {"success": False, "error": "No URL provided"}
@@ -720,33 +761,44 @@ def hostnames_config(shared_state):
720
761
  content = resp.text
721
762
  except requests.RequestException as e:
722
763
  info(f"Failed to fetch hostnames URL: {e}")
723
- return {"success": False, "error": "Failed to fetch URL. Check the console log for details."}
764
+ return {
765
+ "success": False,
766
+ "error": "Failed to fetch URL. Check the console log for details.",
767
+ }
724
768
 
725
769
  # Parse hostnames
726
- allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
770
+ allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
727
771
  results = extract_kv_pairs(content, allowed_keys)
728
772
 
729
773
  if not results:
730
- return {"success": False, "error": "No hostnames found in the provided URL"}
774
+ return {
775
+ "success": False,
776
+ "error": "No hostnames found in the provided URL",
777
+ }
731
778
 
732
779
  # Validate each hostname
733
780
  valid_hostnames = {}
734
781
  invalid_hostnames = {}
735
782
  for shorthand, hostname in results.items():
736
783
  domain_check = extract_valid_hostname(hostname, shorthand)
737
- domain = domain_check.get('domain')
784
+ domain = domain_check.get("domain")
738
785
  if domain:
739
786
  valid_hostnames[shorthand] = domain
740
787
  else:
741
- invalid_hostnames[shorthand] = domain_check.get('message', 'Invalid')
788
+ invalid_hostnames[shorthand] = domain_check.get(
789
+ "message", "Invalid"
790
+ )
742
791
 
743
792
  if not valid_hostnames:
744
- return {"success": False, "error": "No valid hostnames found in the provided URL"}
793
+ return {
794
+ "success": False,
795
+ "error": "No valid hostnames found in the provided URL",
796
+ }
745
797
 
746
798
  return {
747
799
  "success": True,
748
800
  "hostnames": valid_hostnames,
749
- "errors": invalid_hostnames
801
+ "errors": invalid_hostnames,
750
802
  }
751
803
 
752
804
  except Exception as e:
@@ -755,24 +807,28 @@ def hostnames_config(shared_state):
755
807
  @app.get("/api/skip-login")
756
808
  def get_skip_login():
757
809
  """Return list of hostnames with skipped login."""
758
- response.content_type = 'application/json'
810
+ response.content_type = "application/json"
759
811
  skip_db = DataBase("skip_login")
760
- login_required_sites = ['al', 'dd', 'dl', 'nx']
812
+ login_required_sites = ["al", "dd", "dl", "nx"]
761
813
  skipped = []
762
814
  for site in login_required_sites:
763
815
  if skip_db.retrieve(site):
764
816
  skipped.append(site)
765
817
  return {"skipped": skipped}
766
818
 
767
- @app.delete('/api/skip-login/<shorthand>')
819
+ @app.delete("/api/skip-login/<shorthand>")
768
820
  def clear_skip_login(shorthand):
769
821
  DataBase("skip_login").delete(shorthand)
770
822
  return {"success": True}
771
823
 
772
- info(f'Hostnames not set. Starting web server for config at: "{shared_state.values['internal_address']}".')
824
+ info(
825
+ f'Hostnames not set. Starting web server for config at: "{shared_state.values["internal_address"]}".'
826
+ )
773
827
  info("Please set at least one valid hostname there!")
774
828
  quasarr.providers.web_server.temp_server_success = False
775
- return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
829
+ return Server(
830
+ app, listen="0.0.0.0", port=shared_state.values["port"]
831
+ ).serve_temporarily()
776
832
 
777
833
 
778
834
  def hostname_credentials_config(shared_state, shorthand, domain):
@@ -782,18 +838,18 @@ def hostname_credentials_config(shared_state, shorthand, domain):
782
838
 
783
839
  shorthand = shorthand.upper()
784
840
 
785
- @app.get('/')
841
+ @app.get("/")
786
842
  def credentials_form():
787
- form_content = f'''
843
+ form_content = f"""
788
844
  <span>If required register account at: <a href="https://{domain}">{domain}</a>!</span><br><br>
789
845
  <label for="user">Username</label>
790
846
  <input type="text" id="user" name="user" placeholder="User" autocorrect="off"><br>
791
847
 
792
848
  <label for="password">Password</label>
793
849
  <input type="password" id="password" name="password" placeholder="Password"><br>
794
- '''
850
+ """
795
851
 
796
- form_html = f'''
852
+ form_html = f"""
797
853
  <style>
798
854
  .button-row {{
799
855
  display: flex;
@@ -864,14 +920,15 @@ def hostname_credentials_config(shared_state, shorthand, domain):
864
920
  }});
865
921
  }}
866
922
  </script>
867
- '''
923
+ """
868
924
 
869
925
  return render_form(f"Set User and Password for {shorthand}", form_html)
870
926
 
871
- @app.get('/skip-success')
927
+ @app.get("/skip-success")
872
928
  def skip_success():
873
929
  return render_reconnect_success(
874
- f"{shorthand} login skipped. You can configure credentials later in the web UI.")
930
+ f"{shorthand} login skipped. You can configure credentials later in the web UI."
931
+ )
875
932
 
876
933
  @app.post("/api/credentials/<sh>/skip")
877
934
  def skip_credentials(sh):
@@ -888,8 +945,8 @@ def hostname_credentials_config(shared_state, shorthand, domain):
888
945
  if quasarr.providers.web_server.temp_server_success:
889
946
  return render_success(f"{sh} credentials already being processed", 5)
890
947
 
891
- user = request.forms.get('user')
892
- password = request.forms.get('password')
948
+ user = request.forms.get("user")
949
+ password = request.forms.get("password")
893
950
  config = Config(shorthand)
894
951
 
895
952
  error_message = "User and Password wrong or empty!"
@@ -902,25 +959,43 @@ def hostname_credentials_config(shared_state, shorthand, domain):
902
959
  DataBase("skip_login").delete(sh.lower())
903
960
 
904
961
  if sh.lower() == "al":
905
- error_message = ("User and Password wrong or empty.<br><br>"
906
- "Or if you skipped Flaresolverr setup earlier, "
907
- "you must chose to skip login for this site, "
908
- "set up FlareSolverr in the UI and then restart Quasarr!")
909
- if quasarr.providers.sessions.al.create_and_persist_session(shared_state):
962
+ error_message = (
963
+ "User and Password wrong or empty.<br><br>"
964
+ "Or if you skipped Flaresolverr setup earlier, "
965
+ "you must chose to skip login for this site, "
966
+ "set up FlareSolverr in the UI and then restart Quasarr!"
967
+ )
968
+ if quasarr.providers.sessions.al.create_and_persist_session(
969
+ shared_state
970
+ ):
910
971
  quasarr.providers.web_server.temp_server_success = True
911
- return render_reconnect_success(f"{sh} credentials set successfully")
972
+ return render_reconnect_success(
973
+ f"{sh} credentials set successfully"
974
+ )
912
975
  elif sh.lower() == "dd":
913
- if quasarr.providers.sessions.dd.create_and_persist_session(shared_state):
976
+ if quasarr.providers.sessions.dd.create_and_persist_session(
977
+ shared_state
978
+ ):
914
979
  quasarr.providers.web_server.temp_server_success = True
915
- return render_reconnect_success(f"{sh} credentials set successfully")
980
+ return render_reconnect_success(
981
+ f"{sh} credentials set successfully"
982
+ )
916
983
  elif sh.lower() == "dl":
917
- if quasarr.providers.sessions.dl.create_and_persist_session(shared_state):
984
+ if quasarr.providers.sessions.dl.create_and_persist_session(
985
+ shared_state
986
+ ):
918
987
  quasarr.providers.web_server.temp_server_success = True
919
- return render_reconnect_success(f"{sh} credentials set successfully")
988
+ return render_reconnect_success(
989
+ f"{sh} credentials set successfully"
990
+ )
920
991
  elif sh.lower() == "nx":
921
- if quasarr.providers.sessions.nx.create_and_persist_session(shared_state):
992
+ if quasarr.providers.sessions.nx.create_and_persist_session(
993
+ shared_state
994
+ ):
922
995
  quasarr.providers.web_server.temp_server_success = True
923
- return render_reconnect_success(f"{sh} credentials set successfully")
996
+ return render_reconnect_success(
997
+ f"{sh} credentials set successfully"
998
+ )
924
999
  else:
925
1000
  quasarr.providers.web_server.temp_server_success = False
926
1001
  return render_fail(f"Unknown site shorthand! ({sh})")
@@ -931,11 +1006,14 @@ def hostname_credentials_config(shared_state, shorthand, domain):
931
1006
 
932
1007
  info(
933
1008
  f'"{shorthand.lower()}" credentials required to access download links. '
934
- f'Starting web server for config at: "{shared_state.values['internal_address']}".')
1009
+ f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
1010
+ )
935
1011
  info(f"If needed register here: 'https://{domain}'")
936
1012
  info("Please set your credentials now, or skip to allow Quasarr to launch!")
937
1013
  quasarr.providers.web_server.temp_server_success = False
938
- return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
1014
+ return Server(
1015
+ app, listen="0.0.0.0", port=shared_state.values["port"]
1016
+ ).serve_temporarily()
939
1017
 
940
1018
 
941
1019
  def flaresolverr_config(shared_state):
@@ -943,15 +1021,15 @@ def flaresolverr_config(shared_state):
943
1021
  add_no_cache_headers(app)
944
1022
  setup_auth(app)
945
1023
 
946
- @app.get('/')
1024
+ @app.get("/")
947
1025
  def url_form():
948
- form_content = '''
1026
+ form_content = """
949
1027
  <span><a href="https://github.com/FlareSolverr/FlareSolverr?tab=readme-ov-file#installation">A local instance</a>
950
1028
  must be running and reachable to Quasarr!</span><br><br>
951
1029
  <label for="url">FlareSolverr URL</label>
952
1030
  <input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1"><br>
953
- '''
954
- form_html = f'''
1031
+ """
1032
+ form_html = f"""
955
1033
  <style>
956
1034
  .button-row {{
957
1035
  display: flex;
@@ -1022,27 +1100,28 @@ def flaresolverr_config(shared_state):
1022
1100
  }});
1023
1101
  }}
1024
1102
  </script>
1025
- '''
1103
+ """
1026
1104
  return render_form("Set FlareSolverr URL", form_html)
1027
1105
 
1028
- @app.get('/skip-success')
1106
+ @app.get("/skip-success")
1029
1107
  def skip_success():
1030
1108
  return render_reconnect_success(
1031
- "FlareSolverr setup skipped. Some sites (like AL) won't work. You can configure it later in the web UI.")
1109
+ "FlareSolverr setup skipped. Some sites (like AL) won't work. You can configure it later in the web UI."
1110
+ )
1032
1111
 
1033
- @app.post('/api/flaresolverr/skip')
1112
+ @app.post("/api/flaresolverr/skip")
1034
1113
  def skip_flaresolverr():
1035
1114
  """Skip FlareSolverr setup and continue startup."""
1036
1115
  DataBase("skip_flaresolverr").update_store("skipped", "true")
1037
1116
  # Set fallback user agent
1038
1117
  shared_state.update("user_agent", FALLBACK_USER_AGENT)
1039
- info('FlareSolverr setup skipped by user choice')
1118
+ info("FlareSolverr setup skipped by user choice")
1040
1119
  quasarr.providers.web_server.temp_server_success = True
1041
1120
  return {"success": True}
1042
1121
 
1043
- @app.post('/api/flaresolverr')
1122
+ @app.post("/api/flaresolverr")
1044
1123
  def set_flaresolverr_url():
1045
- url = request.forms.get('url').strip()
1124
+ url = request.forms.get("url").strip()
1046
1125
  config = Config("FlareSolverr")
1047
1126
 
1048
1127
  if not url.startswith("http://") and not url.startswith("https://"):
@@ -1054,7 +1133,7 @@ def flaresolverr_config(shared_state):
1054
1133
  data = {
1055
1134
  "cmd": "request.get",
1056
1135
  "url": "http://www.google.com/",
1057
- "maxTimeout": 30000
1136
+ "maxTimeout": 30000,
1058
1137
  }
1059
1138
  resp = requests.post(url, headers=headers, json=data, timeout=30)
1060
1139
  if resp.status_code == 200:
@@ -1063,13 +1142,17 @@ def flaresolverr_config(shared_state):
1063
1142
  DataBase("skip_flaresolverr").delete("skipped")
1064
1143
  print(f'Using Flaresolverr URL: "{url}"')
1065
1144
  quasarr.providers.web_server.temp_server_success = True
1066
- return render_reconnect_success("FlareSolverr URL saved successfully!")
1145
+ return render_reconnect_success(
1146
+ "FlareSolverr URL saved successfully!"
1147
+ )
1067
1148
  except requests.RequestException:
1068
1149
  pass
1069
1150
 
1070
1151
  # on failure, clear any existing value and notify user
1071
1152
  config.save("url", "")
1072
- return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
1153
+ return render_fail(
1154
+ "Could not reach FlareSolverr at that URL (expected HTTP 200)."
1155
+ )
1073
1156
 
1074
1157
  info(
1075
1158
  '"flaresolverr" URL is required for some sites (like AL). '
@@ -1077,7 +1160,9 @@ def flaresolverr_config(shared_state):
1077
1160
  )
1078
1161
  info("Please enter your FlareSolverr URL now, or skip to allow Quasarr to launch!")
1079
1162
  quasarr.providers.web_server.temp_server_success = False
1080
- return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
1163
+ return Server(
1164
+ app, listen="0.0.0.0", port=shared_state.values["port"]
1165
+ ).serve_temporarily()
1081
1166
 
1082
1167
 
1083
1168
  def jdownloader_config(shared_state):
@@ -1085,9 +1170,9 @@ def jdownloader_config(shared_state):
1085
1170
  add_no_cache_headers(app)
1086
1171
  setup_auth(app)
1087
1172
 
1088
- @app.get('/')
1173
+ @app.get("/")
1089
1174
  def jd_form():
1090
- verify_form_html = f'''
1175
+ verify_form_html = f"""
1091
1176
  <span>If required register account at: <a href="https://my.jdownloader.org/login.html#register" target="_blank">
1092
1177
  my.jdownloader.org</a>!</span><br>
1093
1178
 
@@ -1098,9 +1183,17 @@ def jdownloader_config(shared_state):
1098
1183
  <input type="text" id="user" name="user" placeholder="user@example.org" autocorrect="off"><br>
1099
1184
  <label for="pass">Password</label>
1100
1185
  <input type="password" id="pass" name="pass" placeholder="Password"><br>
1101
- {render_button("Verify Credentials",
1102
- "secondary",
1103
- {"id": "verifyButton", "type": "button", "onclick": "verifyCredentials()"})}
1186
+ {
1187
+ render_button(
1188
+ "Verify Credentials",
1189
+ "secondary",
1190
+ {
1191
+ "id": "verifyButton",
1192
+ "type": "button",
1193
+ "onclick": "verifyCredentials()",
1194
+ },
1195
+ )
1196
+ }
1104
1197
  </form>
1105
1198
 
1106
1199
  <p>Some JDownloader settings will be enforced by Quasarr on startup.</p>
@@ -1113,9 +1206,9 @@ def jdownloader_config(shared_state):
1113
1206
  {render_button("Save", "primary", {"type": "submit", "id": "storeBtn"})}
1114
1207
  </form>
1115
1208
  <p><strong>Saving may take a while!</strong></p><br>
1116
- '''
1209
+ """
1117
1210
 
1118
- verify_script = '''
1211
+ verify_script = """
1119
1212
  <script>
1120
1213
  var verifyInProgress = false;
1121
1214
  var storeSubmitted = false;
@@ -1168,21 +1261,23 @@ def jdownloader_config(shared_state):
1168
1261
  return true;
1169
1262
  }
1170
1263
  </script>
1171
- '''
1172
- return render_form("Set your credentials for My JDownloader", verify_form_html, verify_script)
1264
+ """
1265
+ return render_form(
1266
+ "Set your credentials for My JDownloader", verify_form_html, verify_script
1267
+ )
1173
1268
 
1174
1269
  @app.post("/api/verify_jdownloader")
1175
1270
  def verify_jdownloader():
1176
1271
  data = request.json
1177
- username = data['user']
1178
- password = data['pass']
1272
+ username = data["user"]
1273
+ password = data["pass"]
1179
1274
 
1180
1275
  devices = shared_state.get_devices(username, password)
1181
1276
  device_names = []
1182
1277
 
1183
1278
  if devices:
1184
1279
  for device in devices:
1185
- device_names.append(device['name'])
1280
+ device_names.append(device["name"])
1186
1281
 
1187
1282
  if device_names:
1188
1283
  return {"success": True, "devices": device_names}
@@ -1191,26 +1286,29 @@ def jdownloader_config(shared_state):
1191
1286
 
1192
1287
  @app.post("/api/store_jdownloader")
1193
1288
  def store_jdownloader():
1194
- username = request.forms.get('user')
1195
- password = request.forms.get('pass')
1196
- device = request.forms.get('device')
1289
+ username = request.forms.get("user")
1290
+ password = request.forms.get("pass")
1291
+ device = request.forms.get("device")
1197
1292
 
1198
1293
  if username and password and device:
1199
1294
  # Verify connection works before saving credentials
1200
1295
  if shared_state.set_device(username, password, device):
1201
- config = Config('JDownloader')
1202
- config.save('user', username)
1203
- config.save('password', password)
1204
- config.save('device', device)
1296
+ config = Config("JDownloader")
1297
+ config.save("user", username)
1298
+ config.save("password", password)
1299
+ config.save("device", device)
1205
1300
  quasarr.providers.web_server.temp_server_success = True
1206
1301
  return render_reconnect_success("Credentials set")
1207
1302
 
1208
1303
  return render_fail("Could not set credentials!")
1209
1304
 
1210
1305
  info(
1211
- f'My-JDownloader-Credentials not set. '
1212
- f'Starting web server for config at: "{shared_state.values['internal_address']}".')
1306
+ f"My-JDownloader-Credentials not set. "
1307
+ f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
1308
+ )
1213
1309
  info("If needed register here: 'https://my.jdownloader.org/login.html#register'")
1214
1310
  info("Please set your credentials now, to allow Quasarr to launch!")
1215
1311
  quasarr.providers.web_server.temp_server_success = False
1216
- return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
1312
+ return Server(
1313
+ app, listen="0.0.0.0", port=shared_state.values["port"]
1314
+ ).serve_temporarily()