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

quasarr/storage/setup.py CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import os
6
6
  import sys
7
+ from urllib.parse import urlparse
7
8
 
8
9
  import requests
9
10
  from bottle import Bottle, request, response
@@ -14,11 +15,70 @@ import quasarr.providers.sessions.al
14
15
  import quasarr.providers.sessions.dd
15
16
  import quasarr.providers.sessions.dl
16
17
  import quasarr.providers.sessions.nx
17
- from quasarr.providers.html_templates import render_button, render_form, render_success, render_fail
18
+ from quasarr.providers.html_templates import render_button, render_form, render_success, render_fail, \
19
+ render_centered_html
18
20
  from quasarr.providers.log import info
19
21
  from quasarr.providers.shared_state import extract_valid_hostname
22
+ from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys
20
23
  from quasarr.providers.web_server import Server
21
24
  from quasarr.storage.config import Config
25
+ from quasarr.storage.sqlite_database import DataBase
26
+
27
+
28
+ def render_reconnect_success(message, countdown_seconds=3):
29
+ """Render a success page that waits, then polls until the server is back online."""
30
+ button_html = render_button(f"Continuing in {countdown_seconds}...", "secondary",
31
+ {"id": "reconnectBtn", "disabled": "true"})
32
+
33
+ script = f'''
34
+ <script>
35
+ var remaining = {countdown_seconds};
36
+ var btn = document.getElementById('reconnectBtn');
37
+
38
+ var interval = setInterval(function() {{
39
+ remaining--;
40
+ btn.innerText = 'Continuing in ' + remaining + '...';
41
+ if (remaining <= 0) {{
42
+ clearInterval(interval);
43
+ btn.innerText = 'Reconnecting...';
44
+ tryReconnect();
45
+ }}
46
+ }}, 1000);
47
+
48
+ function tryReconnect() {{
49
+ var attempts = 0;
50
+ function attempt() {{
51
+ attempts++;
52
+ fetch('/', {{ method: 'HEAD', cache: 'no-store' }})
53
+ .then(function(response) {{
54
+ if (response.ok) {{
55
+ btn.innerText = 'Connected! Reloading...';
56
+ btn.className = 'btn-primary';
57
+ setTimeout(function() {{ window.location.href = '/'; }}, 500);
58
+ }} else {{
59
+ scheduleRetry();
60
+ }}
61
+ }})
62
+ .catch(function() {{
63
+ scheduleRetry();
64
+ }});
65
+ }}
66
+ function scheduleRetry() {{
67
+ btn.innerText = 'Reconnecting... (attempt ' + attempts + ')';
68
+ setTimeout(attempt, 1000);
69
+ }}
70
+ attempt();
71
+ }}
72
+ </script>
73
+ '''
74
+
75
+ content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
76
+ <h2>✓ Success</h2>
77
+ <p>{message}</p>
78
+ {button_html}
79
+ {script}
80
+ '''
81
+ return render_centered_html(content)
22
82
 
23
83
 
24
84
  def add_no_cache_headers(app):
@@ -81,22 +141,31 @@ def path_config(shared_state):
81
141
  config_path = request.forms.get("config_path")
82
142
  config_path = set_config_path(config_path)
83
143
  quasarr.providers.web_server.temp_server_success = True
84
- return render_success(f'Config path set to: "{config_path}"',
85
- 5)
144
+ return render_reconnect_success(f'Config path set to: "{config_path}"')
86
145
 
87
146
  info(f'Starting web server for config at: "{shared_state.values['internal_address']}".')
88
147
  info("Please set desired config path there!")
89
148
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
90
149
 
91
150
 
92
- def hostname_form_html(shared_state, message):
151
+ def hostname_form_html(shared_state, message, show_restart_button=False, show_skip_management=False):
93
152
  hostname_fields = '''
94
153
  <label for="{id}" style="display:inline-flex; align-items:center; gap:4px;">{label}{img_html}</label>
95
154
  <input type="text" id="{id}" name="{id}" placeholder="example.com" autocorrect="off" autocomplete="off" value="{value}"><br>
96
155
  '''
97
156
 
157
+ skip_indicator = '''
158
+ <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;">
159
+ <span style="color:#dc3545;">⚠️ Login skipped</span>
160
+ <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>
161
+ </div>
162
+ '''
163
+
98
164
  field_html = []
99
165
  hostnames = Config('Hostnames') # Load once outside the loop
166
+ skip_login_db = DataBase("skip_login")
167
+ login_required_sites = ['al', 'dd', 'dl', 'nx']
168
+
100
169
  for label in shared_state.values["sites"]:
101
170
  field_id = label.lower()
102
171
  img_html = ''
@@ -119,32 +188,116 @@ def hostname_form_html(shared_state, message):
119
188
  value=current_value
120
189
  ))
121
190
 
191
+ # Add skip indicator for login-required sites if skip management is enabled
192
+ if show_skip_management and field_id in login_required_sites:
193
+ if current_value and skip_login_db.retrieve(field_id):
194
+ field_html.append(skip_indicator.format(id=field_id))
195
+
122
196
  hostname_form_content = "".join(field_html)
123
197
  button_html = render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})
124
198
 
199
+ # Get stored hostnames URL if available
200
+ stored_url = Config('Settings').get("hostnames_url") or ""
201
+
202
+ # Build restart button HTML if needed
203
+ restart_section = ""
204
+ if show_restart_button:
205
+ restart_section = f'''
206
+ <div class="section-divider" style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--divider-color, #dee2e6);">
207
+ <p style="font-size:0.875rem; color:var(--secondary, #6c757d);">Restart required after changing login-required hostnames (AL, DD, DL, NX)</p>
208
+ {render_button("Restart Quasarr", "secondary", {"type": "button", "onclick": "confirmRestart()"})}
209
+ </div>
210
+ '''
211
+
125
212
  template = """
213
+ <style>
214
+ .url-import-section {{
215
+ border: 1px solid var(--divider-color, #dee2e6);
216
+ border-radius: 0.5rem;
217
+ padding: 1rem;
218
+ margin-bottom: 1.5rem;
219
+ background: var(--code-bg, #f8f9fa);
220
+ }}
221
+ .url-import-section h3 {{
222
+ margin: 0 0 0.75rem 0;
223
+ font-size: 1rem;
224
+ font-weight: 600;
225
+ }}
226
+ .url-import-row {{
227
+ display: flex;
228
+ gap: 0.5rem;
229
+ align-items: stretch;
230
+ }}
231
+ .url-import-row input {{
232
+ flex: 1;
233
+ margin-bottom: 0;
234
+ }}
235
+ .url-import-row button {{
236
+ margin-top: 0;
237
+ white-space: nowrap;
238
+ }}
239
+ .import-status {{
240
+ margin-top: 0.5rem;
241
+ font-size: 0.875rem;
242
+ min-height: 1.25rem;
243
+ }}
244
+ .import-status.success {{ color: #198754; }}
245
+ .import-status.error {{ color: #dc3545; }}
246
+ .import-status.loading {{ color: var(--secondary, #6c757d); }}
247
+ .btn-subtle {{
248
+ background: transparent;
249
+ color: var(--fg-color, #212529);
250
+ border: 1px solid var(--btn-subtle-border, #ced4da);
251
+ padding: 0.25rem 0.5rem;
252
+ border-radius: 0.25rem;
253
+ cursor: pointer;
254
+ font-size: 0.875rem;
255
+ }}
256
+ .btn-subtle:hover {{
257
+ background: var(--btn-subtle-bg, #e9ecef);
258
+ }}
259
+ </style>
260
+
126
261
  <div id="message" style="margin-bottom:0.5em;">{message}</div>
127
262
  <div id="error-msg" style="color:red; margin-bottom:1em;"></div>
128
263
 
264
+ <div class="url-import-section">
265
+ <h3>📥 Import from URL</h3>
266
+ <div class="url-import-row">
267
+ <input type="text" id="hostnamesUrl" placeholder="https://pastebin.com/raw/..." value="{stored_url}" autocorrect="off" autocomplete="off">
268
+ <button type="button" class="btn-secondary" id="importBtn" onclick="importHostnames()">Import</button>
269
+ </div>
270
+ <div id="importStatus" class="import-status"></div>
271
+ <p style="font-size:0.75rem; color:var(--secondary, #6c757d); margin:0.5rem 0 0 0;">
272
+ Paste a URL containing hostname definitions (same format as --hostnames parameter)
273
+ </p>
274
+ </div>
275
+
129
276
  <form action="/api/hostnames" method="post" onsubmit="return validateHostnames(this)">
277
+ <input type="hidden" id="hostnamesUrlHidden" name="hostnames_url" value="{stored_url}">
130
278
  {hostname_form_content}
131
279
  {button}
132
280
  </form>
133
281
 
282
+ {restart_section}
283
+
134
284
  <script>
135
285
  var formSubmitted = false;
286
+
136
287
  function validateHostnames(form) {{
137
288
  if (formSubmitted) return false;
138
289
 
139
290
  var errorDiv = document.getElementById('error-msg');
140
291
  errorDiv.textContent = '';
141
292
 
142
- var inputs = form.querySelectorAll('input[type="text"]');
293
+ var inputs = form.querySelectorAll('input[type="text"]:not(#hostnamesUrl)');
143
294
  for (var i = 0; i < inputs.length; i++) {{
144
295
  if (inputs[i].value.trim() !== '') {{
145
296
  formSubmitted = true;
146
297
  var btn = document.getElementById('submitBtn');
147
298
  if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
299
+ // Sync the URL field to hidden input
300
+ document.getElementById('hostnamesUrlHidden').value = document.getElementById('hostnamesUrl').value.trim();
148
301
  return true;
149
302
  }}
150
303
  }}
@@ -153,12 +306,164 @@ def hostname_form_html(shared_state, message):
153
306
  inputs[0].focus();
154
307
  return false;
155
308
  }}
309
+
310
+ function importHostnames() {{
311
+ var urlInput = document.getElementById('hostnamesUrl');
312
+ var url = urlInput.value.trim();
313
+ var statusDiv = document.getElementById('importStatus');
314
+ var importBtn = document.getElementById('importBtn');
315
+
316
+ if (!url) {{
317
+ statusDiv.className = 'import-status error';
318
+ statusDiv.textContent = 'Please enter a URL';
319
+ return;
320
+ }}
321
+
322
+ statusDiv.className = 'import-status loading';
323
+ statusDiv.textContent = 'Importing...';
324
+ importBtn.disabled = true;
325
+ importBtn.textContent = 'Importing...';
326
+
327
+ fetch('/api/hostnames/import-url', {{
328
+ method: 'POST',
329
+ headers: {{ 'Content-Type': 'application/json' }},
330
+ body: JSON.stringify({{ url: url }})
331
+ }})
332
+ .then(response => response.json())
333
+ .then(data => {{
334
+ importBtn.disabled = false;
335
+ importBtn.textContent = 'Import';
336
+
337
+ if (data.success) {{
338
+ var count = 0;
339
+ for (var key in data.hostnames) {{
340
+ var input = document.getElementById(key);
341
+ if (input) {{
342
+ input.value = data.hostnames[key];
343
+ count++;
344
+ }}
345
+ }}
346
+ statusDiv.className = 'import-status success';
347
+ var msg = 'Imported ' + count + ' hostname(s)';
348
+ if (data.errors && Object.keys(data.errors).length > 0) {{
349
+ msg += ' (' + Object.keys(data.errors).length + ' invalid)';
350
+ }}
351
+ statusDiv.textContent = msg + '. Review and click Save.';
352
+ }} else {{
353
+ statusDiv.className = 'import-status error';
354
+ statusDiv.textContent = data.error || 'Import failed';
355
+ }}
356
+ }})
357
+ .catch(error => {{
358
+ importBtn.disabled = false;
359
+ importBtn.textContent = 'Import';
360
+ statusDiv.className = 'import-status error';
361
+ statusDiv.textContent = 'Network error: ' + error.message;
362
+ }});
363
+ }}
364
+
365
+ function clearSkipLogin(shorthand, btnElement) {{
366
+ fetch('/api/skip-login/' + shorthand, {{ method: 'DELETE' }})
367
+ .then(response => response.json())
368
+ .then(data => {{
369
+ if (data.success) {{
370
+ // Remove the skip indicator using the button's parent
371
+ var indicator = btnElement.closest('.skip-indicator');
372
+ if (indicator) indicator.remove();
373
+ alert('Login requirement restored for ' + shorthand.toUpperCase() + '. Restart Quasarr to be prompted for credentials.');
374
+ }} else {{
375
+ alert('Failed to clear skip preference');
376
+ }}
377
+ }})
378
+ .catch(error => {{
379
+ alert('Error: ' + error.message);
380
+ }});
381
+ }}
382
+
383
+ function confirmRestart() {{
384
+ if (confirm('Restart Quasarr now? Any unsaved changes will be lost.')) {{
385
+ fetch('/api/restart', {{ method: 'POST' }})
386
+ .then(response => response.json())
387
+ .then(data => {{
388
+ if (data.success) {{
389
+ showRestartOverlay();
390
+ }}
391
+ }})
392
+ .catch(error => {{
393
+ // Expected - connection will be lost during restart
394
+ showRestartOverlay();
395
+ }});
396
+ }}
397
+ }}
398
+
399
+ function showRestartOverlay() {{
400
+ document.body.innerHTML = `
401
+ <div style="text-align:center; padding:2rem; font-family:system-ui,-apple-system,sans-serif;">
402
+ <h2>Restarting Quasarr...</h2>
403
+ <p id="restartStatus">Waiting <span id="countdown">10</span> seconds...</p>
404
+ <div id="spinner" style="display:none; margin-top:1rem;">
405
+ <div style="display:inline-block; width:24px; height:24px; border:3px solid #ccc; border-top-color:#333; border-radius:50%; animation:spin 1s linear infinite;"></div>
406
+ <style>@keyframes spin {{ to {{ transform: rotate(360deg); }} }}</style>
407
+ </div>
408
+ </div>
409
+ `;
410
+ startCountdown(10);
411
+ }}
412
+
413
+ function startCountdown(seconds) {{
414
+ var countdownEl = document.getElementById('countdown');
415
+ var statusEl = document.getElementById('restartStatus');
416
+ var spinnerEl = document.getElementById('spinner');
417
+
418
+ var remaining = seconds;
419
+ var interval = setInterval(function() {{
420
+ remaining--;
421
+ if (countdownEl) countdownEl.textContent = remaining;
422
+
423
+ if (remaining <= 0) {{
424
+ clearInterval(interval);
425
+ statusEl.textContent = 'Reconnecting...';
426
+ spinnerEl.style.display = 'block';
427
+ tryReconnect();
428
+ }}
429
+ }}, 1000);
430
+ }}
431
+
432
+ function tryReconnect() {{
433
+ var statusEl = document.getElementById('restartStatus');
434
+ var attempts = 0;
435
+
436
+ function attempt() {{
437
+ attempts++;
438
+ fetch('/', {{ method: 'HEAD', cache: 'no-store' }})
439
+ .then(response => {{
440
+ if (response.ok) {{
441
+ statusEl.textContent = 'Connected! Reloading...';
442
+ setTimeout(function() {{ window.location.href = '/'; }}, 500);
443
+ }} else {{
444
+ scheduleRetry();
445
+ }}
446
+ }})
447
+ .catch(function() {{
448
+ scheduleRetry();
449
+ }});
450
+ }}
451
+
452
+ function scheduleRetry() {{
453
+ statusEl.textContent = 'Reconnecting... (attempt ' + attempts + ')';
454
+ setTimeout(attempt, 1000);
455
+ }}
456
+
457
+ attempt();
458
+ }}
156
459
  </script>
157
460
  """
158
461
  return template.format(
159
462
  message=message,
160
463
  hostname_form_content=hostname_form_content,
161
- button=button_html
464
+ button=button_html,
465
+ stored_url=stored_url,
466
+ restart_section=restart_section
162
467
  )
163
468
 
164
469
 
@@ -212,6 +517,11 @@ def save_hostnames(shared_state, timeout=5, first_run=True):
212
517
  if old_val != '':
213
518
  hostnames.save(shorthand, '')
214
519
 
520
+ # Handle hostnames URL storage
521
+ hostnames_url = request.forms.get('hostnames_url', '').strip()
522
+ settings_config = Config("Settings")
523
+ settings_config.save("hostnames_url", hostnames_url)
524
+
215
525
  quasarr.providers.web_server.temp_server_success = True
216
526
 
217
527
  # Build success message, include any per-site errors
@@ -227,7 +537,8 @@ def save_hostnames(shared_state, timeout=5, first_run=True):
227
537
  if site.lower() in {'al', 'dd', 'dl', 'nx'}:
228
538
  optional_text += f"{site.upper()}: You must restart Quasarr and follow additional steps to start using this site.<br>"
229
539
 
230
- return render_success(success_msg, timeout, optional_text=optional_text)
540
+ full_message = f"{success_msg}<br><small>{optional_text}</small>"
541
+ return render_reconnect_success(full_message)
231
542
 
232
543
 
233
544
  def hostnames_config(shared_state):
@@ -248,6 +559,63 @@ def hostnames_config(shared_state):
248
559
  def set_hostnames():
249
560
  return save_hostnames(shared_state)
250
561
 
562
+ @app.post("/api/hostnames/import-url")
563
+ def import_hostnames_from_url():
564
+ """Fetch URL and parse hostnames, return JSON for JS to populate fields."""
565
+ response.content_type = 'application/json'
566
+ try:
567
+ data = request.json
568
+ url = data.get('url', '').strip()
569
+
570
+ if not url:
571
+ return {"success": False, "error": "No URL provided"}
572
+
573
+ # Validate URL
574
+ parsed = urlparse(url)
575
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
576
+ return {"success": False, "error": "Invalid URL format"}
577
+
578
+ if "/raw/eX4Mpl3" in url:
579
+ return {"success": False, "error": "Example URL detected. Please provide a real URL."}
580
+
581
+ # Fetch content
582
+ try:
583
+ resp = requests.get(url, timeout=15)
584
+ resp.raise_for_status()
585
+ content = resp.text
586
+ except requests.RequestException as e:
587
+ return {"success": False, "error": f"Failed to fetch URL: {str(e)}"}
588
+
589
+ # Parse hostnames
590
+ allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
591
+ results = extract_kv_pairs(content, allowed_keys)
592
+
593
+ if not results:
594
+ return {"success": False, "error": "No hostnames found in the provided URL"}
595
+
596
+ # Validate each hostname
597
+ valid_hostnames = {}
598
+ invalid_hostnames = {}
599
+ for shorthand, hostname in results.items():
600
+ domain_check = extract_valid_hostname(hostname, shorthand)
601
+ domain = domain_check.get('domain')
602
+ if domain:
603
+ valid_hostnames[shorthand] = domain
604
+ else:
605
+ invalid_hostnames[shorthand] = domain_check.get('message', 'Invalid')
606
+
607
+ if not valid_hostnames:
608
+ return {"success": False, "error": "No valid hostnames found in the provided URL"}
609
+
610
+ return {
611
+ "success": True,
612
+ "hostnames": valid_hostnames,
613
+ "errors": invalid_hostnames
614
+ }
615
+
616
+ except Exception as e:
617
+ return {"success": False, "error": f"Error: {str(e)}"}
618
+
251
619
  info(f'Hostnames not set. Starting web server for config at: "{shared_state.values['internal_address']}".')
252
620
  info("Please set at least one valid hostname there!")
253
621
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
@@ -271,10 +639,39 @@ def hostname_credentials_config(shared_state, shorthand, domain):
271
639
  '''
272
640
 
273
641
  form_html = f'''
642
+ <style>
643
+ .button-row {{
644
+ display: flex;
645
+ gap: 0.75rem;
646
+ justify-content: center;
647
+ flex-wrap: wrap;
648
+ margin-top: 1rem;
649
+ }}
650
+ .btn-warning {{
651
+ background-color: #ffc107;
652
+ color: #212529;
653
+ border: 1.5px solid #d39e00;
654
+ padding: 0.5rem 1rem;
655
+ font-size: 1rem;
656
+ border-radius: 0.5rem;
657
+ font-weight: 500;
658
+ cursor: pointer;
659
+ }}
660
+ .btn-warning:hover {{
661
+ background-color: #e0a800;
662
+ border-color: #c69500;
663
+ }}
664
+ </style>
274
665
  <form id="credentialsForm" action="/api/credentials/{shorthand}" method="post" onsubmit="return handleSubmit(this)">
275
666
  {form_content}
276
- {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
667
+ <div class="button-row">
668
+ {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
669
+ <button type="button" class="btn-warning" id="skipBtn" onclick="skipLogin()">Skip for now</button>
670
+ </div>
277
671
  </form>
672
+ <p style="font-size:0.875rem; color:var(--secondary, #6c757d); margin-top:1rem;">
673
+ Skipping will allow Quasarr to start, but this site won't work until credentials are provided.
674
+ </p>
278
675
  <script>
279
676
  var formSubmitted = false;
280
677
  function handleSubmit(form) {{
@@ -282,13 +679,54 @@ def hostname_credentials_config(shared_state, shorthand, domain):
282
679
  formSubmitted = true;
283
680
  var btn = document.getElementById('submitBtn');
284
681
  if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
682
+ document.getElementById('skipBtn').disabled = true;
285
683
  return true;
286
684
  }}
685
+ function skipLogin() {{
686
+ if (formSubmitted) return;
687
+ formSubmitted = true;
688
+ var skipBtn = document.getElementById('skipBtn');
689
+ var submitBtn = document.getElementById('submitBtn');
690
+ if (skipBtn) {{ skipBtn.disabled = true; skipBtn.textContent = 'Skipping...'; }}
691
+ if (submitBtn) {{ submitBtn.disabled = true; }}
692
+
693
+ fetch('/api/credentials/{shorthand}/skip', {{ method: 'POST' }})
694
+ .then(response => {{
695
+ if (response.ok) {{
696
+ window.location.href = '/skip-success';
697
+ }} else {{
698
+ alert('Failed to skip login');
699
+ formSubmitted = false;
700
+ if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
701
+ if (submitBtn) {{ submitBtn.disabled = false; }}
702
+ }}
703
+ }})
704
+ .catch(error => {{
705
+ alert('Error: ' + error.message);
706
+ formSubmitted = false;
707
+ if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
708
+ if (submitBtn) {{ submitBtn.disabled = false; }}
709
+ }});
710
+ }}
287
711
  </script>
288
712
  '''
289
713
 
290
714
  return render_form(f"Set User and Password for {shorthand}", form_html)
291
715
 
716
+ @app.get('/skip-success')
717
+ def skip_success():
718
+ return render_reconnect_success(
719
+ f"{shorthand} login skipped. You can configure credentials later in the web UI.")
720
+
721
+ @app.post("/api/credentials/<sh>/skip")
722
+ def skip_credentials(sh):
723
+ """Skip login for this hostname and continue startup."""
724
+ sh_lower = sh.lower()
725
+ DataBase("skip_login").update_store(sh_lower, "true")
726
+ info(f'Login for "{sh}" skipped by user choice')
727
+ quasarr.providers.web_server.temp_server_success = True
728
+ return {"success": True}
729
+
292
730
  @app.post("/api/credentials/<sh>")
293
731
  def set_credentials(sh):
294
732
  # Guard against duplicate submissions (e.g., double-click)
@@ -303,22 +741,25 @@ def hostname_credentials_config(shared_state, shorthand, domain):
303
741
  config.save("user", user)
304
742
  config.save("password", password)
305
743
 
744
+ # Clear any skip preference since we now have credentials
745
+ DataBase("skip_login").delete(sh.lower())
746
+
306
747
  if sh.lower() == "al":
307
748
  if quasarr.providers.sessions.al.create_and_persist_session(shared_state):
308
749
  quasarr.providers.web_server.temp_server_success = True
309
- return render_success(f"{sh} credentials set successfully", 5)
750
+ return render_reconnect_success(f"{sh} credentials set successfully")
310
751
  elif sh.lower() == "dd":
311
752
  if quasarr.providers.sessions.dd.create_and_persist_session(shared_state):
312
753
  quasarr.providers.web_server.temp_server_success = True
313
- return render_success(f"{sh} credentials set successfully", 5)
754
+ return render_reconnect_success(f"{sh} credentials set successfully")
314
755
  elif sh.lower() == "dl":
315
756
  if quasarr.providers.sessions.dl.create_and_persist_session(shared_state):
316
757
  quasarr.providers.web_server.temp_server_success = True
317
- return render_success(f"{sh} credentials set successfully", 5)
758
+ return render_reconnect_success(f"{sh} credentials set successfully")
318
759
  elif sh.lower() == "nx":
319
760
  if quasarr.providers.sessions.nx.create_and_persist_session(shared_state):
320
761
  quasarr.providers.web_server.temp_server_success = True
321
- return render_success(f"{sh} credentials set successfully", 5)
762
+ return render_reconnect_success(f"{sh} credentials set successfully")
322
763
  else:
323
764
  quasarr.providers.web_server.temp_server_success = False
324
765
  return render_fail(f"Unknown site shorthand! ({sh})")
@@ -331,7 +772,7 @@ def hostname_credentials_config(shared_state, shorthand, domain):
331
772
  f'"{shorthand.lower()}" credentials required to access download links. '
332
773
  f'Starting web server for config at: "{shared_state.values['internal_address']}".')
333
774
  info(f"If needed register here: 'https://{domain}'")
334
- info("Please set your credentials now, to allow Quasarr to launch!")
775
+ info("Please set your credentials now, or skip to allow Quasarr to launch!")
335
776
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
336
777
 
337
778
 
@@ -386,7 +827,7 @@ def flaresolverr_config(shared_state):
386
827
  config.save("url", url)
387
828
  print(f'Using Flaresolverr URL: "{url}"')
388
829
  quasarr.providers.web_server.temp_server_success = True
389
- return render_success("FlareSolverr URL saved successfully!", 5)
830
+ return render_reconnect_success("FlareSolverr URL saved successfully!")
390
831
  except requests.RequestException:
391
832
  pass
392
833
 
@@ -524,7 +965,7 @@ def jdownloader_config(shared_state):
524
965
  config.save('password', password)
525
966
  config.save('device', device)
526
967
  quasarr.providers.web_server.temp_server_success = True
527
- return render_success("Credentials set", 15)
968
+ return render_reconnect_success("Credentials set")
528
969
 
529
970
  return render_fail("Could not set credentials!")
530
971