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

@@ -120,6 +120,7 @@ def connect_to_jd(jd, user, password, device_name):
120
120
  info("Error connecting to JDownloader: " + str(e).strip())
121
121
  return False
122
122
  if not device or not isinstance(device, (type, Jddevice)):
123
+ info(f'Device "{device_name}" not found. Available devices may differ or be offline.')
123
124
  return False
124
125
  else:
125
126
  device.downloadcontroller.get_current_state() # request forces direct_connection info update
@@ -8,7 +8,7 @@ import requests
8
8
 
9
9
 
10
10
  def get_version():
11
- return "1.26.0"
11
+ return "1.26.2"
12
12
 
13
13
 
14
14
  def get_latest_version():
quasarr/storage/setup.py CHANGED
@@ -6,7 +6,7 @@ import os
6
6
  import sys
7
7
 
8
8
  import requests
9
- from bottle import Bottle, request
9
+ from bottle import Bottle, request, response
10
10
 
11
11
  import quasarr
12
12
  import quasarr.providers.html_images as images
@@ -21,19 +21,40 @@ from quasarr.providers.web_server import Server
21
21
  from quasarr.storage.config import Config
22
22
 
23
23
 
24
+ def add_no_cache_headers(app):
25
+ """Add hooks to prevent browser caching of setup pages."""
26
+
27
+ @app.hook('after_request')
28
+ def set_no_cache():
29
+ response.set_header('Cache-Control', 'no-cache, no-store, must-revalidate')
30
+ response.set_header('Pragma', 'no-cache')
31
+ response.set_header('Expires', '0')
32
+
33
+
24
34
  def path_config(shared_state):
25
35
  app = Bottle()
36
+ add_no_cache_headers(app)
26
37
 
27
38
  current_path = os.path.dirname(os.path.abspath(sys.argv[0]))
28
39
 
29
40
  @app.get('/')
30
41
  def config_form():
31
42
  config_form_html = f'''
32
- <form action="/api/config" method="post">
43
+ <form action="/api/config" method="post" onsubmit="return handleSubmit(this)">
33
44
  <label for="config_path">Path</label>
34
45
  <input type="text" id="config_path" name="config_path" placeholder="{current_path}"><br>
35
- {render_button("Save", "primary", {"type": "submit"})}
46
+ {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
36
47
  </form>
48
+ <script>
49
+ var formSubmitted = false;
50
+ function handleSubmit(form) {{
51
+ if (formSubmitted) return false;
52
+ formSubmitted = true;
53
+ var btn = document.getElementById('submitBtn');
54
+ if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
55
+ return true;
56
+ }}
57
+ </script>
37
58
  '''
38
59
  return render_form("Press 'Save' to set desired path for configuration",
39
60
  config_form_html)
@@ -99,7 +120,7 @@ def hostname_form_html(shared_state, message):
99
120
  ))
100
121
 
101
122
  hostname_form_content = "".join(field_html)
102
- button_html = render_button("Save", "primary", {"type": "submit"})
123
+ button_html = render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})
103
124
 
104
125
  template = """
105
126
  <div id="message" style="margin-bottom:0.5em;">{message}</div>
@@ -111,13 +132,19 @@ def hostname_form_html(shared_state, message):
111
132
  </form>
112
133
 
113
134
  <script>
135
+ var formSubmitted = false;
114
136
  function validateHostnames(form) {{
137
+ if (formSubmitted) return false;
138
+
115
139
  var errorDiv = document.getElementById('error-msg');
116
140
  errorDiv.textContent = '';
117
141
 
118
142
  var inputs = form.querySelectorAll('input[type="text"]');
119
143
  for (var i = 0; i < inputs.length; i++) {{
120
144
  if (inputs[i].value.trim() !== '') {{
145
+ formSubmitted = true;
146
+ var btn = document.getElementById('submitBtn');
147
+ if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
121
148
  return true;
122
149
  }}
123
150
  }}
@@ -205,13 +232,14 @@ def save_hostnames(shared_state, timeout=5, first_run=True):
205
232
 
206
233
  def hostnames_config(shared_state):
207
234
  app = Bottle()
235
+ add_no_cache_headers(app)
208
236
 
209
237
  @app.get('/')
210
238
  def hostname_form():
211
239
  message = """<p>
212
240
  If you're having trouble setting this up, take a closer look at
213
- <a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank" rel="noopener noreferrer">
214
- step one of these instructions.
241
+ <a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#quasarr" target="_blank" rel="noopener noreferrer">
242
+ the instructions.
215
243
  </a>
216
244
  </p>"""
217
245
  return render_form("Set at least one valid hostname", hostname_form_html(shared_state, message))
@@ -227,6 +255,7 @@ def hostnames_config(shared_state):
227
255
 
228
256
  def hostname_credentials_config(shared_state, shorthand, domain):
229
257
  app = Bottle()
258
+ add_no_cache_headers(app)
230
259
 
231
260
  shorthand = shorthand.upper()
232
261
 
@@ -242,16 +271,30 @@ def hostname_credentials_config(shared_state, shorthand, domain):
242
271
  '''
243
272
 
244
273
  form_html = f'''
245
- <form action="/api/credentials/{shorthand}" method="post">
274
+ <form id="credentialsForm" action="/api/credentials/{shorthand}" method="post" onsubmit="return handleSubmit(this)">
246
275
  {form_content}
247
- {render_button("Save", "primary", {"type": "submit"})}
276
+ {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
248
277
  </form>
278
+ <script>
279
+ var formSubmitted = false;
280
+ function handleSubmit(form) {{
281
+ if (formSubmitted) return false;
282
+ formSubmitted = true;
283
+ var btn = document.getElementById('submitBtn');
284
+ if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
285
+ return true;
286
+ }}
287
+ </script>
249
288
  '''
250
289
 
251
290
  return render_form(f"Set User and Password for {shorthand}", form_html)
252
291
 
253
292
  @app.post("/api/credentials/<sh>")
254
293
  def set_credentials(sh):
294
+ # Guard against duplicate submissions (e.g., double-click)
295
+ if quasarr.providers.web_server.temp_server_success:
296
+ return render_success(f"{sh} credentials already being processed", 5)
297
+
255
298
  user = request.forms.get('user')
256
299
  password = request.forms.get('password')
257
300
  config = Config(shorthand)
@@ -294,6 +337,7 @@ def hostname_credentials_config(shared_state, shorthand, domain):
294
337
 
295
338
  def flaresolverr_config(shared_state):
296
339
  app = Bottle()
340
+ add_no_cache_headers(app)
297
341
 
298
342
  @app.get('/')
299
343
  def url_form():
@@ -304,10 +348,20 @@ def flaresolverr_config(shared_state):
304
348
  <input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1"><br>
305
349
  '''
306
350
  form_html = f'''
307
- <form action="/api/flaresolverr" method="post">
351
+ <form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
308
352
  {form_content}
309
- {render_button("Save", "primary", {"type": "submit"})}
353
+ {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
310
354
  </form>
355
+ <script>
356
+ var formSubmitted = false;
357
+ function handleSubmit(form) {{
358
+ if (formSubmitted) return false;
359
+ formSubmitted = true;
360
+ var btn = document.getElementById('submitBtn');
361
+ if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
362
+ return true;
363
+ }}
364
+ </script>
311
365
  '''
312
366
  return render_form("Set FlareSolverr URL", form_html)
313
367
 
@@ -316,6 +370,9 @@ def flaresolverr_config(shared_state):
316
370
  url = request.forms.get('url').strip()
317
371
  config = Config("FlareSolverr")
318
372
 
373
+ if not url.startswith("http://") and not url.startswith("https://"):
374
+ url = "http://" + url
375
+
319
376
  if url:
320
377
  try:
321
378
  headers = {"Content-Type": "application/json"}
@@ -324,8 +381,8 @@ def flaresolverr_config(shared_state):
324
381
  "url": "http://www.google.com/",
325
382
  "maxTimeout": 30000
326
383
  }
327
- response = requests.post(url, headers=headers, json=data, timeout=30)
328
- if response.status_code == 200:
384
+ resp = requests.post(url, headers=headers, json=data, timeout=30)
385
+ if resp.status_code == 200:
329
386
  config.save("url", url)
330
387
  print(f'Using Flaresolverr URL: "{url}"')
331
388
  quasarr.providers.web_server.temp_server_success = True
@@ -347,6 +404,7 @@ def flaresolverr_config(shared_state):
347
404
 
348
405
  def jdownloader_config(shared_state):
349
406
  app = Bottle()
407
+ add_no_cache_headers(app)
350
408
 
351
409
  @app.get('/')
352
410
  def jd_form():
@@ -368,19 +426,26 @@ def jdownloader_config(shared_state):
368
426
 
369
427
  <p>Some JDownloader settings will be enforced by Quasarr on startup.</p>
370
428
 
371
- <form action="/api/store_jdownloader" method="post" id="deviceForm" style="display: none;">
429
+ <form action="/api/store_jdownloader" method="post" id="deviceForm" style="display: none;" onsubmit="return handleStoreSubmit(this)">
372
430
  <input type="hidden" id="hiddenUser" name="user">
373
431
  <input type="hidden" id="hiddenPass" name="pass">
374
432
  <label for="device">JDownloader</label>
375
433
  <select id="device" name="device"></select><br>
376
- {render_button("Save", "primary", {"type": "submit"})}
434
+ {render_button("Save", "primary", {"type": "submit", "id": "storeBtn"})}
377
435
  </form>
378
436
  <p><strong>Saving may take a while!</strong></p><br>
379
437
  '''
380
438
 
381
439
  verify_script = '''
382
440
  <script>
441
+ var verifyInProgress = false;
442
+ var storeSubmitted = false;
383
443
  function verifyCredentials() {
444
+ if (verifyInProgress) return;
445
+ verifyInProgress = true;
446
+ var btn = document.getElementById('verifyButton');
447
+ if (btn) { btn.disabled = true; btn.textContent = 'Verifying...'; }
448
+
384
449
  var user = document.getElementById('user').value;
385
450
  var pass = document.getElementById('pass').value;
386
451
  fetch('/api/verify_jdownloader', {
@@ -406,12 +471,23 @@ def jdownloader_config(shared_state):
406
471
  document.getElementById('deviceForm').style.display = 'block';
407
472
  } else {
408
473
  alert('Fehler! Bitte die Zugangsdaten überprüfen.');
474
+ verifyInProgress = false;
475
+ if (btn) { btn.disabled = false; btn.textContent = 'Verify Credentials'; }
409
476
  }
410
477
  })
411
478
  .catch((error) => {
412
479
  console.error('Error:', error);
480
+ verifyInProgress = false;
481
+ if (btn) { btn.disabled = false; btn.textContent = 'Verify Credentials'; }
413
482
  });
414
483
  }
484
+ function handleStoreSubmit(form) {
485
+ if (storeSubmitted) return false;
486
+ storeSubmitted = true;
487
+ var btn = document.getElementById('storeBtn');
488
+ if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
489
+ return true;
490
+ }
415
491
  </script>
416
492
  '''
417
493
  return render_form("Set your credentials for My JDownloader", verify_form_html, verify_script)
@@ -440,21 +516,15 @@ def jdownloader_config(shared_state):
440
516
  password = request.forms.get('pass')
441
517
  device = request.forms.get('device')
442
518
 
443
- config = Config('JDownloader')
444
-
445
519
  if username and password and device:
446
- config.save('user', username)
447
- config.save('password', password)
448
- config.save('device', device)
449
-
450
- if not shared_state.set_device_from_config():
451
- config.save('user', "")
452
- config.save('password', "")
453
- config.save('device', "")
454
- else:
520
+ # Verify connection works before saving credentials
521
+ if shared_state.set_device(username, password, device):
522
+ config = Config('JDownloader')
523
+ config.save('user', username)
524
+ config.save('password', password)
525
+ config.save('device', device)
455
526
  quasarr.providers.web_server.temp_server_success = True
456
- return render_success("Credentials set",
457
- 15)
527
+ return render_success("Credentials set", 15)
458
528
 
459
529
  return render_fail("Could not set credentials!")
460
530
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quasarr
3
- Version: 1.26.0
3
+ Version: 1.26.2
4
4
  Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
5
5
  Home-page: https://github.com/rix1337/Quasarr
6
6
  Author: rix1337
@@ -31,15 +31,15 @@ quasarr/downloads/sources/wx.py,sha256=EygMfkgBMZYj3tSk4gvj5DcojkRswGhY_y8FMPNnV
31
31
  quasarr/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  quasarr/providers/cloudflare.py,sha256=mJxTYM7JjwIjdO6U_GyVj-dyug6XdD0-ObkYTaPtFVg,7028
33
33
  quasarr/providers/html_images.py,sha256=2n82gTJg7E7q2ytPFN4FWouYTIlmPYu_iHFtG7uktIA,28482
34
- quasarr/providers/html_templates.py,sha256=AMDo8cRxlEpnY8ZpwGI9TPdMLLWA0ePsYhNcIIu26zU,9496
34
+ quasarr/providers/html_templates.py,sha256=YMwdi7l_tHL0-qsUnwi4aPrE5Q6ZDxbjsPIfr-6uemY,10265
35
35
  quasarr/providers/imdb_metadata.py,sha256=10L4kZkt6Fg0HGdNcc6KCtIQHRYEqdarLyaMVN6mT8w,4843
36
36
  quasarr/providers/log.py,sha256=_g5RwtfuksARXnvryhsngzoJyFcNzj6suqd3ndqZM0Y,313
37
37
  quasarr/providers/myjd_api.py,sha256=Z3PEiO3c3UfDSr4Up5rgwTAnjloWHb-H1RkJ6BLKZv8,34140
38
38
  quasarr/providers/notifications.py,sha256=bohT-6yudmFnmZMc3BwCGX0n1HdzSVgQG_LDZm_38dI,4630
39
- quasarr/providers/obfuscated.py,sha256=SzhDZ-ndWtIpBulm-LR8J5Lw4LCgWOCzxB07JsBPvKg,1350372
40
- quasarr/providers/shared_state.py,sha256=1NUKtm9YXWPvN64By2O2OYH5ke5TmBkJSbSxiNczgtU,29849
39
+ quasarr/providers/obfuscated.py,sha256=6hnjiNEdrMQaHNgRRBFVZej8Zld-LaihZfQe6YM8aD8,1344189
40
+ quasarr/providers/shared_state.py,sha256=-TIiH2lkCfovq7bzUZicpUjXEjS87ZHCcevsFgySOqw,29944
41
41
  quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
42
- quasarr/providers/version.py,sha256=MN2yssTCIGNfUfOS_cfcuJlSom7BATAfTkt7EtHX-TA,4004
42
+ quasarr/providers/version.py,sha256=gZK-UAU9UlateFaOTImyoErz9l4su5BuOADF-ryh3ss,4004
43
43
  quasarr/providers/web_server.py,sha256=AYd0KRxdDWMBr87BP8wlSMuL4zZo0I_rY-vHBai6Pfg,1688
44
44
  quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  quasarr/providers/sessions/al.py,sha256=F1d76eAJcbTvb6YvAIlQ4gfrYC_256QAKiWEQcuWe8k,10612
@@ -67,11 +67,11 @@ quasarr/search/sources/wd.py,sha256=O02j3irSlVw2qES82g_qHuavAk-njjSRH1dHSCnOUas,
67
67
  quasarr/search/sources/wx.py,sha256=HIGElGKoBkxBdgoYiaC70T4Y22xRUmgMzzbJpFq1cxw,12897
68
68
  quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
69
  quasarr/storage/config.py,sha256=hOI7vvIo1YaML3dtAkTmp0HSedWF6brVhRk3d8pJtXI,6300
70
- quasarr/storage/setup.py,sha256=Gf7e125KlHsyu-hhq3uFfH7N6i7-8DDONGcYJX0haLs,18261
70
+ quasarr/storage/setup.py,sha256=V_dWfyCYxRE_ahmBtD-DmHOKP2L6brBcVYydPRcHjns,21448
71
71
  quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
72
- quasarr-1.26.0.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
73
- quasarr-1.26.0.dist-info/METADATA,sha256=bXWnG37Ff8gVuu9F6os6mj4kMaRffuRcZk6lPxlTsxA,12805
74
- quasarr-1.26.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
- quasarr-1.26.0.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
76
- quasarr-1.26.0.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
77
- quasarr-1.26.0.dist-info/RECORD,,
72
+ quasarr-1.26.2.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
73
+ quasarr-1.26.2.dist-info/METADATA,sha256=aYxG2KoX7GLS2AFIaUw2kHc75UIOyEdjmgh4UqouJow,12805
74
+ quasarr-1.26.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ quasarr-1.26.2.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
76
+ quasarr-1.26.2.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
77
+ quasarr-1.26.2.dist-info/RECORD,,