quasarr 2.4.8__py3-none-any.whl → 2.4.10__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 (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.10.dist-info}/METADATA +4 -3
  72. quasarr-2.4.10.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.10.dist-info}/WHEEL +0 -0
  75. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/entry_points.txt +0 -0
  76. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/licenses/LICENSE +0 -0
quasarr/__init__.py CHANGED
@@ -15,13 +15,26 @@ import requests
15
15
  import quasarr.providers.web_server
16
16
  from quasarr.api import get_api
17
17
  from quasarr.providers import shared_state, version
18
- from quasarr.providers.log import info, debug
18
+ from quasarr.providers.log import debug, info
19
19
  from quasarr.providers.notifications import send_discord_message
20
- from quasarr.providers.utils import extract_allowed_keys, extract_kv_pairs, is_valid_url, check_ip, check_flaresolverr, \
21
- validate_address, Unbuffered, FALLBACK_USER_AGENT
20
+ from quasarr.providers.utils import (
21
+ FALLBACK_USER_AGENT,
22
+ Unbuffered,
23
+ check_flaresolverr,
24
+ check_ip,
25
+ extract_allowed_keys,
26
+ extract_kv_pairs,
27
+ is_valid_url,
28
+ validate_address,
29
+ )
22
30
  from quasarr.storage.config import Config, get_clean_hostnames
23
- from quasarr.storage.setup import path_config, hostnames_config, hostname_credentials_config, flaresolverr_config, \
24
- jdownloader_config
31
+ from quasarr.storage.setup import (
32
+ flaresolverr_config,
33
+ hostname_credentials_config,
34
+ hostnames_config,
35
+ jdownloader_config,
36
+ path_config,
37
+ )
25
38
  from quasarr.storage.sqlite_database import DataBase
26
39
 
27
40
 
@@ -33,10 +46,17 @@ def run():
33
46
 
34
47
  parser = argparse.ArgumentParser()
35
48
  parser.add_argument("--port", help="Desired Port, defaults to 8080")
36
- parser.add_argument("--internal_address", help="Must be provided when running in Docker")
37
- parser.add_argument("--external_address", help="External address for CAPTCHA notifications")
49
+ parser.add_argument(
50
+ "--internal_address", help="Must be provided when running in Docker"
51
+ )
52
+ parser.add_argument(
53
+ "--external_address", help="External address for CAPTCHA notifications"
54
+ )
38
55
  parser.add_argument("--discord", help="Discord Webhook URL")
39
- parser.add_argument("--hostnames", help="Public HTTP(s) Link that contains hostnames definition.")
56
+ parser.add_argument(
57
+ "--hostnames",
58
+ help="Public HTTP(s) Link that contains hostnames definition.",
59
+ )
40
60
  arguments = parser.parse_args()
41
61
 
42
62
  sys.stdout = Unbuffered(sys.stdout)
@@ -47,25 +67,31 @@ def run():
47
67
  └────────────────────────────────────┘""")
48
68
 
49
69
  print("\n===== Recommended Services =====")
50
- print('For convenient universal premium downloads use: "https://linksnappy.com/?ref=397097"')
51
70
  print(
52
- 'Sponsors get automated CAPTCHA solutions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper"')
71
+ 'For convenient universal premium downloads use: "https://linksnappy.com/?ref=397097"'
72
+ )
73
+ print(
74
+ 'Sponsors get automated CAPTCHA solutions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper"'
75
+ )
53
76
 
54
77
  print("\n===== Startup Info =====")
55
- port = int('8080')
78
+ port = int("8080")
56
79
  config_path = ""
57
- if os.environ.get('DOCKER'):
80
+ if os.environ.get("DOCKER"):
58
81
  config_path = "/config"
59
82
  if not arguments.internal_address:
60
83
  print(
61
- "You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://192.168.1.1:8080")
62
- print("The local URL will be used by Radarr/Sonarr to connect to Quasarr")
84
+ "You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://192.168.1.1:8080"
85
+ )
86
+ print(
87
+ "The local URL will be used by Radarr/Sonarr to connect to Quasarr"
88
+ )
63
89
  print("Stopping Quasarr...")
64
90
  sys.exit(1)
65
91
  else:
66
92
  if arguments.port:
67
93
  port = int(arguments.port)
68
- internal_address = f'http://{check_ip()}:{port}'
94
+ internal_address = f"http://{check_ip()}:{port}"
69
95
 
70
96
  if arguments.internal_address:
71
97
  internal_address = arguments.internal_address
@@ -92,14 +118,13 @@ def run():
92
118
  temp_file = tempfile.TemporaryFile(dir=config_path)
93
119
  temp_file.close()
94
120
  except Exception as e:
95
- print(f'Could not access "{config_path}": {e}"'
96
- f'Stopping Quasarr...')
121
+ print(f'Could not access "{config_path}": {e}"Stopping Quasarr...')
97
122
  sys.exit(1)
98
123
 
99
124
  shared_state.set_files(config_path)
100
125
  shared_state.update("config", Config)
101
126
  shared_state.update("database", DataBase)
102
- supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
127
+ supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
103
128
  shared_state.update("sites", [key.upper() for key in supported_hostnames])
104
129
  # Set fallback user agent immediately so it's available while background check runs
105
130
  shared_state.update("user_agent", FALLBACK_USER_AGENT)
@@ -117,31 +142,46 @@ def run():
117
142
  print(f"Extracting hostnames from {hostnames_link}...")
118
143
  allowed_keys = supported_hostnames
119
144
  max_keys = len(allowed_keys)
120
- shorthand_list = ', '.join(
121
- [f'"{key}"' for key in allowed_keys[:-1]]) + ' and ' + f'"{allowed_keys[-1]}"'
122
- print(f'There are up to {max_keys} hostnames currently supported: {shorthand_list}')
145
+ shorthand_list = (
146
+ ", ".join([f'"{key}"' for key in allowed_keys[:-1]])
147
+ + " and "
148
+ + f'"{allowed_keys[-1]}"'
149
+ )
150
+ print(
151
+ f"There are up to {max_keys} hostnames currently supported: {shorthand_list}"
152
+ )
123
153
  data = requests.get(hostnames_link).text
124
154
  results = extract_kv_pairs(data, allowed_keys)
125
155
 
126
156
  extracted_hostnames = 0
127
157
 
128
158
  if results:
129
- hostnames = Config('Hostnames')
159
+ hostnames = Config("Hostnames")
130
160
  for shorthand, hostname in results.items():
131
- domain_check = shared_state.extract_valid_hostname(hostname, shorthand)
132
- valid_domain = domain_check.get('domain', None)
161
+ domain_check = shared_state.extract_valid_hostname(
162
+ hostname, shorthand
163
+ )
164
+ valid_domain = domain_check.get("domain", None)
133
165
  if valid_domain:
134
166
  hostnames.save(shorthand, hostname)
135
167
  extracted_hostnames += 1
136
- print(f'Hostname for "{shorthand}" successfully set to "{hostname}"')
168
+ print(
169
+ f'Hostname for "{shorthand}" successfully set to "{hostname}"'
170
+ )
137
171
  else:
138
- print(f'Skipping invalid hostname for "{shorthand}" ("{hostname}")')
172
+ print(
173
+ f'Skipping invalid hostname for "{shorthand}" ("{hostname}")'
174
+ )
139
175
  if extracted_hostnames == max_keys:
140
- print(f'All {max_keys} hostnames successfully extracted!')
141
- print('You can now remove the hostnames link from the command line / environment variable.')
176
+ print(f"All {max_keys} hostnames successfully extracted!")
177
+ print(
178
+ "You can now remove the hostnames link from the command line / environment variable."
179
+ )
142
180
  else:
143
- print(f'No Hostnames found at "{hostnames_link}". '
144
- 'Ensure to pass a plain hostnames list, not html or json!')
181
+ print(
182
+ f'No Hostnames found at "{hostnames_link}". '
183
+ "Ensure to pass a plain hostnames list, not html or json!"
184
+ )
145
185
  else:
146
186
  print(f'Invalid hostnames URL: "{hostnames_link}"')
147
187
  except Exception as e:
@@ -151,42 +191,48 @@ def run():
151
191
  if not hostnames:
152
192
  hostnames_config(shared_state)
153
193
  hostnames = get_clean_hostnames(shared_state)
154
- print(f"You have [{len(hostnames)} of {len(Config._DEFAULT_CONFIG['Hostnames'])}] supported hostnames set up")
194
+ print(
195
+ f"You have [{len(hostnames)} of {len(Config._DEFAULT_CONFIG['Hostnames'])}] supported hostnames set up"
196
+ )
155
197
  print(f"For efficiency it is recommended to set up as few hostnames as needed.")
156
198
 
157
199
  # Check credentials for login-required hostnames
158
200
  skip_login_db = DataBase("skip_login")
159
- login_required_sites = ['al', 'dd', 'dl', 'nx']
201
+ login_required_sites = ["al", "dd", "dl", "nx"]
160
202
 
161
203
  quasarr.providers.web_server.temp_server_success = False
162
204
 
163
205
  for site in login_required_sites:
164
- hostname = Config('Hostnames').get(site)
206
+ hostname = Config("Hostnames").get(site)
165
207
  if hostname:
166
208
  site_config = Config(site.upper())
167
- user = site_config.get('user')
168
- password = site_config.get('password')
209
+ user = site_config.get("user")
210
+ password = site_config.get("password")
169
211
  if not user or not password:
170
212
  skip_val = skip_login_db.retrieve(site)
171
213
  if skip_val and str(skip_val).lower() == "true":
172
214
  info(f'"{site.upper()}" login skipped by user preference')
173
215
  else:
174
- info(f'"{site.upper()}" credentials missing. Launching setup...')
216
+ info(
217
+ f'"{site.upper()}" credentials missing. Launching setup...'
218
+ )
175
219
  quasarr.providers.web_server.temp_server_success = False
176
- hostname_credentials_config(shared_state, site.upper(), hostname)
220
+ hostname_credentials_config(
221
+ shared_state, site.upper(), hostname
222
+ )
177
223
 
178
224
  # Check FlareSolverr configuration
179
225
  skip_flaresolverr_db = DataBase("skip_flaresolverr")
180
226
  flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
181
- flaresolverr_url = Config('FlareSolverr').get('url')
227
+ flaresolverr_url = Config("FlareSolverr").get("url")
182
228
 
183
229
  if not flaresolverr_url and not flaresolverr_skipped:
184
230
  flaresolverr_config(shared_state)
185
231
 
186
- config = Config('JDownloader')
187
- user = config.get('user')
188
- password = config.get('password')
189
- device = config.get('device')
232
+ config = Config("JDownloader")
233
+ user = config.get("user")
234
+ password = config.get("password")
235
+ device = config.get("device")
190
236
 
191
237
  if not user or not password or not device:
192
238
  jdownloader_config(shared_state)
@@ -194,7 +240,7 @@ def run():
194
240
  print("\n===== Notifications =====")
195
241
  discord_url = ""
196
242
  if arguments.discord:
197
- discord_webhook_pattern = r'^https://discord\.com/api/webhooks/\d+/[\w-]+$'
243
+ discord_webhook_pattern = r"^https://discord\.com/api/webhooks/\d+/[\w-]+$"
198
244
  if re.match(discord_webhook_pattern, arguments.discord):
199
245
  shared_state.update("webhook", arguments.discord)
200
246
  print(f"Using Discord Webhook URL for notifications.")
@@ -206,45 +252,49 @@ def run():
206
252
  shared_state.update("discord", discord_url)
207
253
 
208
254
  print("\n===== API Information =====")
209
- api_key = Config('API').get('key')
255
+ api_key = Config("API").get("key")
210
256
  if not api_key:
211
257
  api_key = shared_state.generate_api_key()
212
258
 
213
- print('Setup instructions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions"')
214
- print(f'URL: "{shared_state.values['internal_address']}"')
259
+ print(
260
+ 'Setup instructions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions"'
261
+ )
262
+ print(f'URL: "{shared_state.values["internal_address"]}"')
215
263
  print(f'API Key: "{api_key}" (without quotes)')
216
264
 
217
265
  if external_address != internal_address:
218
266
  print(f'External URL: "{shared_state.values["external_address"]}"')
219
267
 
220
268
  print("\n===== Quasarr Info Log =====")
221
- if os.getenv('DEBUG'):
269
+ if os.getenv("DEBUG"):
222
270
  print("===== / Debug Log =====")
223
271
 
224
272
  protected = shared_state.get_db("protected").retrieve_all_titles()
225
273
  if protected:
226
274
  package_count = len(protected)
227
- info(f'CAPTCHA-Solution required for {package_count} package{'s' if package_count > 1 else ''} at: '
228
- f'"{shared_state.values["external_address"]}/captcha"!')
275
+ info(
276
+ f"CAPTCHA-Solution required for {package_count} package{'s' if package_count > 1 else ''} at: "
277
+ f'"{shared_state.values["external_address"]}/captcha"!'
278
+ )
229
279
 
230
280
  flaresolverr = multiprocessing.Process(
231
281
  target=flaresolverr_checker,
232
282
  args=(shared_state_dict, shared_state_lock),
233
- daemon=True
283
+ daemon=True,
234
284
  )
235
285
  flaresolverr.start()
236
286
 
237
287
  jdownloader = multiprocessing.Process(
238
288
  target=jdownloader_connection,
239
289
  args=(shared_state_dict, shared_state_lock),
240
- daemon=True
290
+ daemon=True,
241
291
  )
242
292
  jdownloader.start()
243
293
 
244
294
  updater = multiprocessing.Process(
245
295
  target=update_checker,
246
296
  args=(shared_state_dict, shared_state_lock),
247
- daemon=True
297
+ daemon=True,
248
298
  )
249
299
  updater.start()
250
300
 
@@ -262,27 +312,31 @@ def flaresolverr_checker(shared_state_dict, shared_state_lock):
262
312
  skip_flaresolverr_db = DataBase("skip_flaresolverr")
263
313
  flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
264
314
 
265
- flaresolverr_url = Config('FlareSolverr').get('url')
315
+ flaresolverr_url = Config("FlareSolverr").get("url")
266
316
 
267
317
  # If FlareSolverr is not configured and not skipped, it means it's the first run
268
318
  # and the user needs to be prompted via the WebUI.
269
319
  # This background process should NOT block or prompt the user.
270
320
  # It should only check and log the status.
271
321
  if not flaresolverr_url and not flaresolverr_skipped:
272
- info('FlareSolverr URL not configured. Please configure it via the WebUI.')
273
- info('Some sites (AL) will not work without FlareSolverr.')
322
+ info("FlareSolverr URL not configured. Please configure it via the WebUI.")
323
+ info("Some sites (AL) will not work without FlareSolverr.")
274
324
  return # Exit the checker, it will be re-checked if user configures it later
275
325
 
276
326
  if flaresolverr_skipped:
277
- info('FlareSolverr setup skipped by user preference')
278
- info('Some sites (AL) will not work without FlareSolverr. Configure it later in the web UI.')
327
+ info("FlareSolverr setup skipped by user preference")
328
+ info(
329
+ "Some sites (AL) will not work without FlareSolverr. Configure it later in the web UI."
330
+ )
279
331
  elif flaresolverr_url:
280
332
  info(f'Checking FlareSolverr at URL: "{flaresolverr_url}"')
281
333
  flaresolverr_check = check_flaresolverr(shared_state, flaresolverr_url)
282
334
  if flaresolverr_check:
283
- info(f'FlareSolverr connection successful. Using User-Agent: "{shared_state.values["user_agent"]}"')
335
+ info(
336
+ f'FlareSolverr connection successful. Using User-Agent: "{shared_state.values["user_agent"]}"'
337
+ )
284
338
  else:
285
- info('FlareSolverr check failed - using fallback user agent')
339
+ info("FlareSolverr check failed - using fallback user agent")
286
340
  # Fallback user agent is already set in main process, but we log it
287
341
  info(f'User Agent (fallback): "{FALLBACK_USER_AGENT}"')
288
342
 
@@ -309,16 +363,18 @@ def update_checker(shared_state_dict, shared_state_lock):
309
363
  info(f'Please manually check: "{link}" for more information!')
310
364
  update_available = None
311
365
 
312
- if update_available and shared_state.values["last_checked_version"] != update_available:
366
+ if (
367
+ update_available
368
+ and shared_state.values["last_checked_version"] != update_available
369
+ ):
313
370
  shared_state.update("last_checked_version", update_available)
314
371
  info(message)
315
372
  info(f"Please update to {update_available} as soon as possible!")
316
373
  info(f'Release notes at: "{link}"')
317
- update_available = {
318
- "version": update_available,
319
- "link": link
320
- }
321
- send_discord_message(shared_state, message, "quasarr_update", details=update_available)
374
+ update_available = {"version": update_available, "link": link}
375
+ send_discord_message(
376
+ shared_state, message, "quasarr_update", details=update_available
377
+ )
322
378
 
323
379
  # wait one hour before next check
324
380
  time.sleep(60 * 60)
@@ -332,22 +388,30 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
332
388
 
333
389
  shared_state.set_device_from_config()
334
390
 
335
- connection_established = shared_state.get_device() and shared_state.get_device().name
391
+ connection_established = (
392
+ shared_state.get_device() and shared_state.get_device().name
393
+ )
336
394
  if not connection_established:
337
395
  i = 0
338
396
  while i < 10:
339
397
  i += 1
340
- info(f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"')
398
+ info(
399
+ f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"'
400
+ )
341
401
  time.sleep(60)
342
402
  shared_state.set_device_from_config()
343
- connection_established = shared_state.get_device() and shared_state.get_device().name
403
+ connection_established = (
404
+ shared_state.get_device() and shared_state.get_device().name
405
+ )
344
406
  if connection_established:
345
407
  break
346
408
 
347
409
  try:
348
- info(f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"')
410
+ info(
411
+ f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"'
412
+ )
349
413
  except Exception as e:
350
- info(f'Error connecting to JDownloader: {e}! Stopping Quasarr!')
414
+ info(f"Error connecting to JDownloader: {e}! Stopping Quasarr!")
351
415
  sys.exit(1)
352
416
 
353
417
  try:
quasarr/api/__init__.py CHANGED
@@ -12,9 +12,13 @@ from quasarr.api.packages import setup_packages_routes
12
12
  from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
13
13
  from quasarr.api.statistics import setup_statistics
14
14
  from quasarr.providers import shared_state
15
- from quasarr.providers.auth import add_auth_routes, add_auth_hook, show_logout_link
15
+ from quasarr.providers.auth import add_auth_hook, add_auth_routes, show_logout_link
16
16
  from quasarr.providers.hostname_issues import get_all_hostname_issues
17
- from quasarr.providers.html_templates import render_button, render_centered_html, render_success
17
+ from quasarr.providers.html_templates import (
18
+ render_button,
19
+ render_centered_html,
20
+ render_success,
21
+ )
18
22
  from quasarr.providers.web_server import Server
19
23
  from quasarr.storage.config import Config
20
24
  from quasarr.storage.sqlite_database import DataBase
@@ -27,10 +31,11 @@ def get_api(shared_state_dict, shared_state_lock):
27
31
 
28
32
  # Auth: routes must come first, then hook
29
33
  add_auth_routes(app)
30
- add_auth_hook(app,
31
- whitelist_prefixes=['/api', '/api/', '/sponsors_helper/', '/download/'],
32
- whitelist_suffixes=['.user.js']
33
- )
34
+ add_auth_hook(
35
+ app,
36
+ whitelist_prefixes=["/api", "/api/", "/sponsors_helper/", "/download/"],
37
+ whitelist_suffixes=[".user.js"],
38
+ )
34
39
 
35
40
  setup_arr_routes(app)
36
41
  setup_captcha_routes(app)
@@ -39,10 +44,10 @@ def get_api(shared_state_dict, shared_state_lock):
39
44
  setup_sponsors_helper_routes(app)
40
45
  setup_packages_routes(app)
41
46
 
42
- @app.get('/')
47
+ @app.get("/")
43
48
  def index():
44
49
  protected = shared_state.get_db("protected").retrieve_all_titles()
45
- api_key = Config('API').get('key')
50
+ api_key = Config("API").get("key")
46
51
 
47
52
  # Get quick status summary
48
53
  try:
@@ -52,10 +57,10 @@ def get_api(shared_state_dict, shared_state_lock):
52
57
  jd_connected = False
53
58
 
54
59
  # Calculate hostname status
55
- hostnames_config = Config('Hostnames')
60
+ hostnames_config = Config("Hostnames")
56
61
  skip_login_db = DataBase("skip_login")
57
62
  hostname_issues = get_all_hostname_issues()
58
- login_required_sites = ['al', 'dd', 'dl', 'nx']
63
+ login_required_sites = ["al", "dd", "dl", "nx"]
59
64
 
60
65
  working_count = 0
61
66
  total_count = 0
@@ -81,26 +86,30 @@ def get_api(shared_state_dict, shared_state_lock):
81
86
 
82
87
  # Determine status
83
88
  if total_count == 0:
84
- hostname_status_class = 'error'
85
- hostname_status_emoji = '⚫️'
86
- hostname_status_text = 'No hostnames configured'
89
+ hostname_status_class = "error"
90
+ hostname_status_emoji = "⚫️"
91
+ hostname_status_text = "No hostnames configured"
87
92
  elif working_count == 0:
88
- hostname_status_class = 'error'
89
- hostname_status_emoji = '🔴'
90
- hostname_status_text = f'0/{total_count} hostnames operational'
93
+ hostname_status_class = "error"
94
+ hostname_status_emoji = "🔴"
95
+ hostname_status_text = f"0/{total_count} hostnames operational"
91
96
  elif working_count < total_count:
92
- hostname_status_class = 'warning'
93
- hostname_status_emoji = '🟡'
94
- hostname_status_text = f'{working_count}/{total_count} hostnames operational'
97
+ hostname_status_class = "warning"
98
+ hostname_status_emoji = "🟡"
99
+ hostname_status_text = (
100
+ f"{working_count}/{total_count} hostnames operational"
101
+ )
95
102
  else:
96
- hostname_status_class = 'success'
97
- hostname_status_emoji = '🟢'
98
- hostname_status_text = f'{working_count}/{total_count} hostnames operational'
103
+ hostname_status_class = "success"
104
+ hostname_status_emoji = "🟢"
105
+ hostname_status_text = (
106
+ f"{working_count}/{total_count} hostnames operational"
107
+ )
99
108
 
100
109
  # CAPTCHA banner
101
110
  captcha_hint = ""
102
111
  if protected:
103
- plural = 's' if len(protected) > 1 else ''
112
+ plural = "s" if len(protected) > 1 else ""
104
113
  captcha_hint = f"""
105
114
  <div class="alert alert-warning">
106
115
  <span class="alert-icon">🔒</span>
@@ -109,7 +118,7 @@ def get_api(shared_state_dict, shared_state_lock):
109
118
  {"" if shared_state.values.get("helper_active") else '<br><a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" target="_blank">Sponsors get automated CAPTCHA solutions!</a>'}
110
119
  </div>
111
120
  <div class="alert-action">
112
- {render_button(f"Solve CAPTCHA{plural}", 'primary', {'onclick': "location.href='/captcha'"})}
121
+ {render_button(f"Solve CAPTCHA{plural}", "primary", {"onclick": "location.href='/captcha'"})}
113
122
  </div>
114
123
  </div>
115
124
  """
@@ -117,8 +126,8 @@ def get_api(shared_state_dict, shared_state_lock):
117
126
  # Status bars
118
127
  status_bars = f"""
119
128
  <div class="status-bar">
120
- <span class="status-pill {'success' if jd_connected else 'error'}">
121
- {'' if jd_connected else ''} JDownloader {'connected' if jd_connected else 'disconnected'}
129
+ <span class="status-pill {"success" if jd_connected else "error"}">
130
+ {"" if jd_connected else ""} JDownloader {"connected" if jd_connected else "disconnected"}
122
131
  </span>
123
132
  <span class="status-pill {hostname_status_class}">
124
133
  {hostname_status_emoji} {hostname_status_text}
@@ -160,7 +169,7 @@ def get_api(shared_state_dict, shared_state_lock):
160
169
  <div class="input-group">
161
170
  <label>URL</label>
162
171
  <div class="input-row">
163
- <input id="urlInput" type="text" readonly value="{shared_state.values['internal_address']}" />
172
+ <input id="urlInput" type="text" readonly value="{shared_state.values["internal_address"]}" />
164
173
  <button id="copyUrl" type="button">Copy</button>
165
174
  </div>
166
175
  </div>
@@ -471,12 +480,12 @@ def get_api(shared_state_dict, shared_state_lock):
471
480
  </script>
472
481
  """
473
482
  # Add logout link for form auth
474
- logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ''
483
+ logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ""
475
484
  return render_centered_html(info, footer_content=logout_html)
476
485
 
477
- @app.get('/regenerate-api-key')
486
+ @app.get("/regenerate-api-key")
478
487
  def regenerate_api_key():
479
488
  shared_state.generate_api_key()
480
- return render_success(f'API Key replaced!', 5)
489
+ return render_success(f"API Key replaced!", 5)
481
490
 
482
- Server(app, listen='0.0.0.0', port=shared_state.values["port"]).serve_forever()
491
+ Server(app, listen="0.0.0.0", port=shared_state.values["port"]).serve_forever()