quasarr 2.4.10__py3-none-any.whl → 2.5.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/__init__.py +3 -3
- quasarr/api/__init__.py +7 -7
- quasarr/api/captcha/__init__.py +1 -1
- quasarr/api/config/__init__.py +22 -169
- quasarr/api/sponsors_helper/__init__.py +2 -2
- quasarr/downloads/linkcrypters/hide.py +1 -1
- quasarr/downloads/packages/__init__.py +5 -5
- quasarr/downloads/sources/al.py +1 -1
- quasarr/downloads/sources/dw.py +1 -1
- quasarr/downloads/sources/sf.py +1 -1
- quasarr/providers/notifications.py +1 -1
- quasarr/providers/shared_state.py +0 -6
- quasarr/providers/version.py +1 -1
- quasarr/search/__init__.py +86 -15
- quasarr/search/sources/al.py +0 -5
- quasarr/search/sources/he.py +1 -2
- quasarr/storage/setup.py +517 -236
- quasarr/storage/sqlite_database.py +1 -1
- {quasarr-2.4.10.dist-info → quasarr-2.5.0.dist-info}/METADATA +1 -1
- {quasarr-2.4.10.dist-info → quasarr-2.5.0.dist-info}/RECORD +23 -23
- {quasarr-2.4.10.dist-info → quasarr-2.5.0.dist-info}/WHEEL +0 -0
- {quasarr-2.4.10.dist-info → quasarr-2.5.0.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.10.dist-info → quasarr-2.5.0.dist-info}/licenses/LICENSE +0 -0
quasarr/storage/setup.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import sys
|
|
7
8
|
from urllib.parse import urlparse
|
|
8
9
|
|
|
@@ -28,6 +29,7 @@ from quasarr.providers.log import info
|
|
|
28
29
|
from quasarr.providers.shared_state import extract_valid_hostname
|
|
29
30
|
from quasarr.providers.utils import (
|
|
30
31
|
FALLBACK_USER_AGENT,
|
|
32
|
+
check_flaresolverr,
|
|
31
33
|
extract_allowed_keys,
|
|
32
34
|
extract_kv_pairs,
|
|
33
35
|
)
|
|
@@ -189,11 +191,9 @@ def _escape_js_for_html_attr(s):
|
|
|
189
191
|
)
|
|
190
192
|
|
|
191
193
|
|
|
192
|
-
def hostname_form_html(
|
|
193
|
-
shared_state, message, show_restart_button=False, show_skip_management=False
|
|
194
|
-
):
|
|
194
|
+
def hostname_form_html(shared_state, message, show_skip_management=False):
|
|
195
195
|
hostname_fields = """
|
|
196
|
-
<label for="{id}" onclick="showStatusDetail(\'{id}\', \'{label}\', \'{status}\', \'{error_details_for_modal}\', \'{timestamp}\', \'{operation}\', \'{url}\')"
|
|
196
|
+
<label for="{id}" onclick="showStatusDetail(\'{id}\', \'{label}\', \'{status}\', \'{error_details_for_modal}\', \'{timestamp}\', \'{operation}\', \'{url}\', \'{user}\', \'{password}\', {supports_login})"
|
|
197
197
|
style="cursor:pointer; display:inline-flex; align-items:center; gap:4px;" title="{status_title}">
|
|
198
198
|
<span class="status-indicator" id="status-{id}" data-status="{status}">{status_emoji}</span>
|
|
199
199
|
{label}
|
|
@@ -203,8 +203,7 @@ def hostname_form_html(
|
|
|
203
203
|
|
|
204
204
|
skip_indicator = """
|
|
205
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;">
|
|
206
|
-
<span style="color:#dc3545;">⚠️ Login skipped
|
|
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 & require login</button>
|
|
206
|
+
<span style="color:#dc3545;">⚠️ Login skipped. Please click header to enable!</span>
|
|
208
207
|
</div>
|
|
209
208
|
"""
|
|
210
209
|
|
|
@@ -258,6 +257,16 @@ def hostname_form_html(
|
|
|
258
257
|
status_title = "Working normally"
|
|
259
258
|
error_details_for_modal = "Configured and working normally."
|
|
260
259
|
|
|
260
|
+
# Get credentials
|
|
261
|
+
user = ""
|
|
262
|
+
password = ""
|
|
263
|
+
supports_login = "false"
|
|
264
|
+
if field_id in login_required_sites:
|
|
265
|
+
supports_login = "true"
|
|
266
|
+
site_config = Config(field_id.upper())
|
|
267
|
+
user = site_config.get("user") or ""
|
|
268
|
+
password = site_config.get("password") or ""
|
|
269
|
+
|
|
261
270
|
field_html.append(
|
|
262
271
|
hostname_fields.format(
|
|
263
272
|
id=field_id,
|
|
@@ -272,6 +281,9 @@ def hostname_form_html(
|
|
|
272
281
|
timestamp=timestamp,
|
|
273
282
|
operation=_escape_js_for_html_attr(operation),
|
|
274
283
|
url=_escape_js_for_html_attr(current_value),
|
|
284
|
+
user=_escape_js_for_html_attr(user),
|
|
285
|
+
password=_escape_js_for_html_attr(password),
|
|
286
|
+
supports_login=supports_login,
|
|
275
287
|
)
|
|
276
288
|
)
|
|
277
289
|
|
|
@@ -288,15 +300,9 @@ def hostname_form_html(
|
|
|
288
300
|
# Get stored hostnames URL if available
|
|
289
301
|
stored_url = Config("Settings").get("hostnames_url") or ""
|
|
290
302
|
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
restart_section = f"""
|
|
295
|
-
<div class="section-divider" style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--divider-color, #dee2e6);">
|
|
296
|
-
<p style="font-size:0.875rem; color:var(--secondary, #6c757d);">Restart required after changing login-required hostnames (AL, DD, DL, NX)</p>
|
|
297
|
-
{render_button("Restart Quasarr", "secondary", {"type": "button", "onclick": "confirmRestart()"})}
|
|
298
|
-
</div>
|
|
299
|
-
"""
|
|
303
|
+
# Check if FlareSolverr is skipped
|
|
304
|
+
skip_flaresolverr_db = DataBase("skip_flaresolverr")
|
|
305
|
+
is_flaresolverr_skipped = bool(skip_flaresolverr_db.retrieve("skipped"))
|
|
300
306
|
|
|
301
307
|
template = """
|
|
302
308
|
<style>
|
|
@@ -376,10 +382,9 @@ def hostname_form_html(
|
|
|
376
382
|
{button}
|
|
377
383
|
</form>
|
|
378
384
|
|
|
379
|
-
{restart_section}
|
|
380
|
-
|
|
381
385
|
<script>
|
|
382
386
|
var formSubmitted = false;
|
|
387
|
+
var isFlaresolverrSkipped = {is_flaresolverr_skipped};
|
|
383
388
|
|
|
384
389
|
function validateHostnames(form) {{
|
|
385
390
|
if (formSubmitted) return false;
|
|
@@ -468,23 +473,6 @@ def hostname_form_html(
|
|
|
468
473
|
}});
|
|
469
474
|
}}
|
|
470
475
|
|
|
471
|
-
function clearSkipLogin(shorthand, btnElement) {{
|
|
472
|
-
fetch('/api/skip-login/' + shorthand, {{ method: 'DELETE' }})
|
|
473
|
-
.then(response => response.json())
|
|
474
|
-
.then(data => {{
|
|
475
|
-
if (data.success) {{
|
|
476
|
-
var indicator = btnElement.closest('.skip-indicator');
|
|
477
|
-
if (indicator) indicator.remove();
|
|
478
|
-
showStatusDetail(shorthand, shorthand.toUpperCase(), 'info', 'Login requirement restored. Restart Quasarr to be prompted for credentials.', '', '', '');
|
|
479
|
-
}} else {{
|
|
480
|
-
showStatusDetail(shorthand, shorthand.toUpperCase(), 'error', 'Failed to clear skip preference', '', '', '');
|
|
481
|
-
}}
|
|
482
|
-
}})
|
|
483
|
-
.catch(error => {{
|
|
484
|
-
showStatusDetail(shorthand, shorthand.toUpperCase(), 'error', 'Error: ' + error.message, '', '', '');
|
|
485
|
-
}});
|
|
486
|
-
}}
|
|
487
|
-
|
|
488
476
|
function confirmRestart() {{
|
|
489
477
|
showModal('Restart Quasarr?', 'Are you sure you want to restart Quasarr now? Any unsaved changes will be lost.',
|
|
490
478
|
`<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
|
@@ -569,7 +557,7 @@ def hostname_form_html(
|
|
|
569
557
|
}}
|
|
570
558
|
</script>
|
|
571
559
|
<script>
|
|
572
|
-
function showStatusDetail(id, label, status, error_details, timestamp, operation, url) {{
|
|
560
|
+
function showStatusDetail(id, label, status, error_details, timestamp, operation, url, user, password, supports_login) {{
|
|
573
561
|
var statusTextMap = {{
|
|
574
562
|
ok: 'Operational',
|
|
575
563
|
error: 'Error',
|
|
@@ -611,7 +599,41 @@ def hostname_form_html(
|
|
|
611
599
|
}}
|
|
612
600
|
}}
|
|
613
601
|
|
|
614
|
-
var
|
|
602
|
+
var credentials_html = '';
|
|
603
|
+
if (url && supports_login) {{
|
|
604
|
+
var flaresolverrWarning = '';
|
|
605
|
+
if (id === 'al' && isFlaresolverrSkipped) {{
|
|
606
|
+
flaresolverrWarning = `
|
|
607
|
+
<div style="margin-bottom: 1rem; padding: 0.75rem; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 0.25rem; color: #856404; font-size: 0.875rem;">
|
|
608
|
+
<strong>⚠️ FlareSolverr Required</strong><br>
|
|
609
|
+
This site requires FlareSolverr, but it was skipped. You must configure it first.
|
|
610
|
+
<div style="margin-top: 0.5rem;">
|
|
611
|
+
<button class="btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" onclick="window.location.href='/flaresolverr'">Configure FlareSolverr</button>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
`;
|
|
615
|
+
}}
|
|
616
|
+
|
|
617
|
+
credentials_html = `
|
|
618
|
+
<div style="margin-top: 1rem; border-top: 1px solid var(--divider-color, #dee2e6); padding-top: 1rem;">
|
|
619
|
+
<h4 style="margin-top:0; font-size:1rem;">Credentials</h4>
|
|
620
|
+
${{flaresolverrWarning}}
|
|
621
|
+
<div style="margin-bottom: 0.5rem;">
|
|
622
|
+
<label style="display:block; font-size: 0.875rem;">Username</label>
|
|
623
|
+
<input type="text" id="cred-user-${{id}}" value="${{user}}" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;">
|
|
624
|
+
</div>
|
|
625
|
+
<div style="margin-bottom: 0.5rem;">
|
|
626
|
+
<label style="display:block; font-size: 0.875rem;">Password</label>
|
|
627
|
+
<input type="password" id="cred-pass-${{id}}" value="${{password}}" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;">
|
|
628
|
+
</div>
|
|
629
|
+
<div id="cred-status-${{id}}" style="margin-bottom: 0.5rem; font-size: 0.875rem; min-height: 1.25em;"></div>
|
|
630
|
+
<button class="btn-primary" onclick="saveAndCheckCredentials('${{id}}')">Check & Save Session</button>
|
|
631
|
+
<div style="margin-top: 1rem; border-bottom: 1px solid var(--divider-color, #dee2e6);"></div>
|
|
632
|
+
</div>
|
|
633
|
+
`;
|
|
634
|
+
}}
|
|
635
|
+
|
|
636
|
+
var content = content_html + timestamp_html + credentials_html;
|
|
615
637
|
var title = '<span>' + (emojiMap[status] || 'ℹ️') + '</span> ' + label + ' - ' + (statusTextMap[status] || status);
|
|
616
638
|
|
|
617
639
|
var buttons = '';
|
|
@@ -621,7 +643,7 @@ def hostname_form_html(
|
|
|
621
643
|
href = 'https://' + href;
|
|
622
644
|
}}
|
|
623
645
|
buttons = `
|
|
624
|
-
<button class="btn-primary" style="margin-right: auto;" onclick="window.open('${{href}}', '_blank')">
|
|
646
|
+
<button class="btn-primary" style="margin-right: auto;" onclick="window.open('${{href}}', '_blank')">Open ${{id.toUpperCase()}}</button>
|
|
625
647
|
<button class="btn-secondary" onclick="closeModal()">Close</button>
|
|
626
648
|
`;
|
|
627
649
|
}} else {{
|
|
@@ -630,6 +652,46 @@ def hostname_form_html(
|
|
|
630
652
|
|
|
631
653
|
showModal(title, content, buttons);
|
|
632
654
|
}}
|
|
655
|
+
|
|
656
|
+
function saveAndCheckCredentials(id) {{
|
|
657
|
+
var user = document.getElementById('cred-user-' + id).value;
|
|
658
|
+
var pass = document.getElementById('cred-pass-' + id).value;
|
|
659
|
+
var statusDiv = document.getElementById('cred-status-' + id);
|
|
660
|
+
|
|
661
|
+
statusDiv.innerHTML = 'Checking...';
|
|
662
|
+
statusDiv.style.color = 'var(--secondary, #6c757d)';
|
|
663
|
+
|
|
664
|
+
fetch('/api/hostnames/check-credentials/' + id, {{
|
|
665
|
+
method: 'POST',
|
|
666
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
667
|
+
body: JSON.stringify({{ user: user, password: pass }})
|
|
668
|
+
}})
|
|
669
|
+
.then(response => response.json())
|
|
670
|
+
.then(data => {{
|
|
671
|
+
if (data.success) {{
|
|
672
|
+
statusDiv.innerHTML = '✅ ' + data.message;
|
|
673
|
+
statusDiv.style.color = '#198754';
|
|
674
|
+
// Update the status indicator in the main list
|
|
675
|
+
var indicator = document.getElementById('status-' + id);
|
|
676
|
+
if (indicator) {{
|
|
677
|
+
indicator.textContent = '🟢';
|
|
678
|
+
indicator.setAttribute('data-status', 'ok');
|
|
679
|
+
}}
|
|
680
|
+
// Remove skip indicator if present
|
|
681
|
+
var skipIndicator = document.getElementById('skip-indicator-' + id);
|
|
682
|
+
if (skipIndicator) {{
|
|
683
|
+
skipIndicator.remove();
|
|
684
|
+
}}
|
|
685
|
+
}} else {{
|
|
686
|
+
statusDiv.innerHTML = '❌ ' + data.message;
|
|
687
|
+
statusDiv.style.color = '#dc3545';
|
|
688
|
+
}}
|
|
689
|
+
}})
|
|
690
|
+
.catch(error => {{
|
|
691
|
+
statusDiv.innerHTML = '❌ Error: ' + error.message;
|
|
692
|
+
statusDiv.style.color = '#dc3545';
|
|
693
|
+
}});
|
|
694
|
+
}}
|
|
633
695
|
</script>
|
|
634
696
|
"""
|
|
635
697
|
return template.format(
|
|
@@ -637,7 +699,7 @@ def hostname_form_html(
|
|
|
637
699
|
hostname_form_content=hostname_form_content,
|
|
638
700
|
button=button_html,
|
|
639
701
|
stored_url=stored_url,
|
|
640
|
-
|
|
702
|
+
is_flaresolverr_skipped="true" if is_flaresolverr_skipped else "false",
|
|
641
703
|
)
|
|
642
704
|
|
|
643
705
|
|
|
@@ -707,16 +769,366 @@ def save_hostnames(shared_state, timeout=5, first_run=True):
|
|
|
707
769
|
else:
|
|
708
770
|
optional_text = "All provided hostnames are valid.<br>"
|
|
709
771
|
|
|
710
|
-
if not first_run:
|
|
711
|
-
# Append restart notice for specific sites that actually changed
|
|
712
|
-
for site in changed_sites:
|
|
713
|
-
if site.lower() in {"al", "dd", "dl", "nx"}:
|
|
714
|
-
optional_text += f"{site.upper()}: You must restart Quasarr and follow additional steps to start using this site.<br>"
|
|
715
|
-
|
|
716
772
|
full_message = f"{success_msg}<br><small>{optional_text}</small>"
|
|
717
773
|
return render_reconnect_success(full_message)
|
|
718
774
|
|
|
719
775
|
|
|
776
|
+
def check_credentials(shared_state, shorthand):
|
|
777
|
+
response.content_type = "application/json"
|
|
778
|
+
try:
|
|
779
|
+
data = request.json
|
|
780
|
+
user = data.get("user")
|
|
781
|
+
password = data.get("password")
|
|
782
|
+
|
|
783
|
+
config = Config(shorthand.upper())
|
|
784
|
+
# Store old credentials to revert if check fails
|
|
785
|
+
old_user = config.get("user")
|
|
786
|
+
old_password = config.get("password")
|
|
787
|
+
|
|
788
|
+
# Temporarily save new credentials for the check
|
|
789
|
+
config.save("user", user)
|
|
790
|
+
config.save("password", password)
|
|
791
|
+
|
|
792
|
+
success = False
|
|
793
|
+
message = "Session check failed"
|
|
794
|
+
|
|
795
|
+
sh_lower = shorthand.lower()
|
|
796
|
+
|
|
797
|
+
# Clear skip login if set (temporarily, will be restored if check fails?)
|
|
798
|
+
# Actually, if user is trying to set credentials, they probably intend to stop skipping.
|
|
799
|
+
# But if check fails, we might want to keep the skip status?
|
|
800
|
+
# For now, let's assume if they try to check credentials, they want to use them.
|
|
801
|
+
|
|
802
|
+
if sh_lower == "al":
|
|
803
|
+
if quasarr.providers.sessions.al.create_and_persist_session(shared_state):
|
|
804
|
+
success = True
|
|
805
|
+
message = "Session valid!"
|
|
806
|
+
else:
|
|
807
|
+
message = "Session check failed (check logs)"
|
|
808
|
+
elif sh_lower == "dd":
|
|
809
|
+
if quasarr.providers.sessions.dd.create_and_persist_session(shared_state):
|
|
810
|
+
success = True
|
|
811
|
+
message = "Session valid!"
|
|
812
|
+
else:
|
|
813
|
+
message = "Session check failed (check logs)"
|
|
814
|
+
elif sh_lower == "dl":
|
|
815
|
+
if quasarr.providers.sessions.dl.create_and_persist_session(shared_state):
|
|
816
|
+
success = True
|
|
817
|
+
message = "Session valid!"
|
|
818
|
+
else:
|
|
819
|
+
message = "Session check failed (check logs)"
|
|
820
|
+
elif sh_lower == "nx":
|
|
821
|
+
if quasarr.providers.sessions.nx.create_and_persist_session(shared_state):
|
|
822
|
+
success = True
|
|
823
|
+
message = "Session valid!"
|
|
824
|
+
else:
|
|
825
|
+
message = "Session check failed (check logs)"
|
|
826
|
+
else:
|
|
827
|
+
success = True
|
|
828
|
+
message = "Credentials saved"
|
|
829
|
+
|
|
830
|
+
if success:
|
|
831
|
+
# If successful, ensure skip login is removed
|
|
832
|
+
DataBase("skip_login").delete(shorthand.lower())
|
|
833
|
+
else:
|
|
834
|
+
# If failed, revert credentials
|
|
835
|
+
config.save("user", old_user)
|
|
836
|
+
config.save("password", old_password)
|
|
837
|
+
|
|
838
|
+
return {"success": success, "message": message}
|
|
839
|
+
|
|
840
|
+
except Exception as e:
|
|
841
|
+
return {"success": False, "message": str(e)}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def import_hostnames_from_url():
|
|
845
|
+
"""Fetch URL and parse hostnames, return JSON for JS to populate fields."""
|
|
846
|
+
response.content_type = "application/json"
|
|
847
|
+
try:
|
|
848
|
+
data = request.json
|
|
849
|
+
url = data.get("url", "").strip()
|
|
850
|
+
|
|
851
|
+
if not url:
|
|
852
|
+
return {"success": False, "error": "No URL provided"}
|
|
853
|
+
|
|
854
|
+
# Validate URL
|
|
855
|
+
parsed = urlparse(url)
|
|
856
|
+
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
857
|
+
return {"success": False, "error": "Invalid URL format"}
|
|
858
|
+
|
|
859
|
+
# Fetch content
|
|
860
|
+
try:
|
|
861
|
+
resp = requests.get(url, timeout=15)
|
|
862
|
+
resp.raise_for_status()
|
|
863
|
+
content = resp.text
|
|
864
|
+
except requests.RequestException as e:
|
|
865
|
+
info(f"Failed to fetch hostnames URL: {e}")
|
|
866
|
+
return {
|
|
867
|
+
"success": False,
|
|
868
|
+
"error": "Failed to fetch URL. Check the console log for details.",
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
# Parse hostnames
|
|
872
|
+
allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
|
|
873
|
+
results = extract_kv_pairs(content, allowed_keys)
|
|
874
|
+
|
|
875
|
+
if not results:
|
|
876
|
+
return {
|
|
877
|
+
"success": False,
|
|
878
|
+
"error": "No hostnames found in the provided URL",
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
# Validate each hostname
|
|
882
|
+
valid_hostnames = {}
|
|
883
|
+
invalid_hostnames = {}
|
|
884
|
+
for shorthand, hostname in results.items():
|
|
885
|
+
domain_check = extract_valid_hostname(hostname, shorthand)
|
|
886
|
+
domain = domain_check.get("domain")
|
|
887
|
+
if domain:
|
|
888
|
+
valid_hostnames[shorthand] = domain
|
|
889
|
+
else:
|
|
890
|
+
invalid_hostnames[shorthand] = domain_check.get("message", "Invalid")
|
|
891
|
+
|
|
892
|
+
if not valid_hostnames:
|
|
893
|
+
return {
|
|
894
|
+
"success": False,
|
|
895
|
+
"error": "No valid hostnames found in the provided URL",
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
"success": True,
|
|
900
|
+
"hostnames": valid_hostnames,
|
|
901
|
+
"errors": invalid_hostnames,
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
except Exception as e:
|
|
905
|
+
return {"success": False, "error": f"Error: {str(e)}"}
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def get_skip_login():
|
|
909
|
+
"""Return list of hostnames with skipped login."""
|
|
910
|
+
response.content_type = "application/json"
|
|
911
|
+
skip_db = DataBase("skip_login")
|
|
912
|
+
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
913
|
+
skipped = []
|
|
914
|
+
for site in login_required_sites:
|
|
915
|
+
if skip_db.retrieve(site):
|
|
916
|
+
skipped.append(site)
|
|
917
|
+
return {"skipped": skipped}
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def clear_skip_login(shorthand):
|
|
921
|
+
"""Clear skip login preference for a hostname."""
|
|
922
|
+
response.content_type = "application/json"
|
|
923
|
+
shorthand = shorthand.lower()
|
|
924
|
+
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
925
|
+
if shorthand not in login_required_sites:
|
|
926
|
+
return {"success": False, "error": f"Invalid shorthand: {shorthand}"}
|
|
927
|
+
|
|
928
|
+
skip_db = DataBase("skip_login")
|
|
929
|
+
skip_db.delete(shorthand)
|
|
930
|
+
info(f'Skip login preference cleared for "{shorthand.upper()}"')
|
|
931
|
+
return {"success": True}
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def save_flaresolverr_url(shared_state, is_setup=False):
|
|
935
|
+
"""Save FlareSolverr URL from web UI."""
|
|
936
|
+
url = request.forms.get("url", "").strip()
|
|
937
|
+
config = Config("FlareSolverr")
|
|
938
|
+
|
|
939
|
+
if not url:
|
|
940
|
+
# If URL is empty, treat it as skipping FlareSolverr
|
|
941
|
+
config.save("url", "")
|
|
942
|
+
DataBase("skip_flaresolverr").update_store("skipped", "true")
|
|
943
|
+
# Set fallback user agent
|
|
944
|
+
shared_state.update("user_agent", FALLBACK_USER_AGENT)
|
|
945
|
+
info("FlareSolverr URL cleared and setup skipped")
|
|
946
|
+
|
|
947
|
+
if is_setup:
|
|
948
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
949
|
+
|
|
950
|
+
return render_reconnect_success("FlareSolverr URL cleared (setup skipped).")
|
|
951
|
+
|
|
952
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
|
953
|
+
url = "http://" + url
|
|
954
|
+
|
|
955
|
+
# Validate URL format
|
|
956
|
+
if not re.search(r"/v\d+$", url):
|
|
957
|
+
return render_fail(
|
|
958
|
+
"FlareSolverr URL must end with /v1 (or similar version path)."
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
try:
|
|
962
|
+
headers = {"Content-Type": "application/json"}
|
|
963
|
+
data = {
|
|
964
|
+
"cmd": "request.get",
|
|
965
|
+
"url": "http://www.google.com/",
|
|
966
|
+
"maxTimeout": 30000,
|
|
967
|
+
}
|
|
968
|
+
resp = requests.post(url, headers=headers, json=data, timeout=30)
|
|
969
|
+
if resp.status_code == 200:
|
|
970
|
+
json_data = resp.json()
|
|
971
|
+
if json_data.get("status") == "ok":
|
|
972
|
+
config.save("url", url)
|
|
973
|
+
# Clear skip preference since we now have a working URL
|
|
974
|
+
DataBase("skip_flaresolverr").delete("skipped")
|
|
975
|
+
|
|
976
|
+
# Update user agent from FlareSolverr response
|
|
977
|
+
solution = json_data.get("solution", {})
|
|
978
|
+
solution_ua = solution.get("userAgent")
|
|
979
|
+
if solution_ua:
|
|
980
|
+
shared_state.update("user_agent", solution_ua)
|
|
981
|
+
|
|
982
|
+
info(f'FlareSolverr URL configured: "{url}"')
|
|
983
|
+
|
|
984
|
+
if is_setup:
|
|
985
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
986
|
+
|
|
987
|
+
return render_reconnect_success("FlareSolverr URL saved successfully!")
|
|
988
|
+
else:
|
|
989
|
+
return render_fail(
|
|
990
|
+
f"FlareSolverr returned unexpected status: {json_data.get('status')}"
|
|
991
|
+
)
|
|
992
|
+
except requests.RequestException:
|
|
993
|
+
return render_fail("Could not reach FlareSolverr!")
|
|
994
|
+
|
|
995
|
+
return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def get_flaresolverr_status_data(shared_state):
|
|
999
|
+
"""Return FlareSolverr configuration status."""
|
|
1000
|
+
response.content_type = "application/json"
|
|
1001
|
+
skip_db = DataBase("skip_flaresolverr")
|
|
1002
|
+
is_skipped = bool(skip_db.retrieve("skipped"))
|
|
1003
|
+
current_url = Config("FlareSolverr").get("url") or ""
|
|
1004
|
+
|
|
1005
|
+
# Test connection if URL is set
|
|
1006
|
+
is_working = False
|
|
1007
|
+
if current_url and not is_skipped:
|
|
1008
|
+
is_working = check_flaresolverr(shared_state, current_url)
|
|
1009
|
+
|
|
1010
|
+
return {"skipped": is_skipped, "url": current_url, "working": is_working}
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def delete_skip_flaresolverr_preference():
|
|
1014
|
+
"""Clear skip FlareSolverr preference."""
|
|
1015
|
+
response.content_type = "application/json"
|
|
1016
|
+
skip_db = DataBase("skip_flaresolverr")
|
|
1017
|
+
skip_db.delete("skipped")
|
|
1018
|
+
info("Skip FlareSolverr preference cleared")
|
|
1019
|
+
return {"success": True}
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def flaresolverr_form_html(shared_state, is_setup=False):
|
|
1023
|
+
skip_db = DataBase("skip_flaresolverr")
|
|
1024
|
+
is_skipped = skip_db.retrieve("skipped")
|
|
1025
|
+
current_url = Config("FlareSolverr").get("url") or ""
|
|
1026
|
+
|
|
1027
|
+
skip_indicator = ""
|
|
1028
|
+
if is_skipped and not is_setup:
|
|
1029
|
+
skip_indicator = """
|
|
1030
|
+
<div class="skip-indicator" style="margin-bottom:1rem; padding:0.75rem; background:var(--code-bg, #f8f9fa); border-radius:0.25rem; font-size:0.875rem;">
|
|
1031
|
+
<span style="color:#dc3545;">⚠️ FlareSolverr setup was skipped</span>
|
|
1032
|
+
<p style="margin:0.5rem 0 0 0; font-size:0.75rem; color:var(--secondary, #6c757d);">
|
|
1033
|
+
Some sites (like AL) won't work until FlareSolverr is configured.
|
|
1034
|
+
</p>
|
|
1035
|
+
</div>
|
|
1036
|
+
"""
|
|
1037
|
+
|
|
1038
|
+
form_content = f'''
|
|
1039
|
+
{skip_indicator}
|
|
1040
|
+
<span><a href="https://github.com/FlareSolverr/FlareSolverr?tab=readme-ov-file#installation" target="_blank">FlareSolverr</a>
|
|
1041
|
+
must be running and reachable to Quasarr for some sites to work.</span><br><br>
|
|
1042
|
+
<label for="url">FlareSolverr URL</label>
|
|
1043
|
+
<input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1" value="{current_url}"><br>
|
|
1044
|
+
'''
|
|
1045
|
+
|
|
1046
|
+
buttons = render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})
|
|
1047
|
+
|
|
1048
|
+
extra_js = ""
|
|
1049
|
+
|
|
1050
|
+
if is_setup:
|
|
1051
|
+
buttons += ' <button type="button" class="btn-warning" id="skipBtn" onclick="skipFlaresolverr()">Skip for now</button>'
|
|
1052
|
+
extra_js = """
|
|
1053
|
+
function skipFlaresolverr() {
|
|
1054
|
+
if (formSubmitted) return;
|
|
1055
|
+
formSubmitted = true;
|
|
1056
|
+
var skipBtn = document.getElementById('skipBtn');
|
|
1057
|
+
var submitBtn = document.getElementById('submitBtn');
|
|
1058
|
+
if (skipBtn) { skipBtn.disabled = true; skipBtn.textContent = 'Skipping...'; }
|
|
1059
|
+
if (submitBtn) { submitBtn.disabled = true; }
|
|
1060
|
+
|
|
1061
|
+
fetch('/api/flaresolverr/skip', { method: 'POST' })
|
|
1062
|
+
.then(response => {
|
|
1063
|
+
if (response.ok) {
|
|
1064
|
+
window.location.href = '/skip-success';
|
|
1065
|
+
} else {
|
|
1066
|
+
showModal('Error', 'Failed to skip FlareSolverr setup');
|
|
1067
|
+
formSubmitted = false;
|
|
1068
|
+
if (skipBtn) { skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }
|
|
1069
|
+
if (submitBtn) { submitBtn.disabled = false; }
|
|
1070
|
+
}
|
|
1071
|
+
})
|
|
1072
|
+
.catch(error => {
|
|
1073
|
+
showModal('Error', 'Error: ' + error.message);
|
|
1074
|
+
formSubmitted = false;
|
|
1075
|
+
if (skipBtn) { skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }
|
|
1076
|
+
if (submitBtn) { submitBtn.disabled = false; }
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
"""
|
|
1080
|
+
|
|
1081
|
+
form_html = f"""
|
|
1082
|
+
<style>
|
|
1083
|
+
.button-row {{
|
|
1084
|
+
display: flex;
|
|
1085
|
+
gap: 0.75rem;
|
|
1086
|
+
justify-content: center;
|
|
1087
|
+
flex-wrap: wrap;
|
|
1088
|
+
margin-top: 1rem;
|
|
1089
|
+
}}
|
|
1090
|
+
.btn-warning {{
|
|
1091
|
+
background-color: #ffc107;
|
|
1092
|
+
color: #212529;
|
|
1093
|
+
border: 1.5px solid #d39e00;
|
|
1094
|
+
padding: 0.5rem 1rem;
|
|
1095
|
+
font-size: 1rem;
|
|
1096
|
+
border-radius: 0.5rem;
|
|
1097
|
+
font-weight: 500;
|
|
1098
|
+
cursor: pointer;
|
|
1099
|
+
}}
|
|
1100
|
+
.btn-warning:hover {{
|
|
1101
|
+
background-color: #e0a800;
|
|
1102
|
+
border-color: #c69500;
|
|
1103
|
+
}}
|
|
1104
|
+
</style>
|
|
1105
|
+
<form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
|
|
1106
|
+
{form_content}
|
|
1107
|
+
<div class="button-row">
|
|
1108
|
+
{buttons}
|
|
1109
|
+
</div>
|
|
1110
|
+
</form>
|
|
1111
|
+
<script>
|
|
1112
|
+
var formSubmitted = false;
|
|
1113
|
+
function handleSubmit(form) {{
|
|
1114
|
+
if (formSubmitted) return false;
|
|
1115
|
+
formSubmitted = true;
|
|
1116
|
+
var btn = document.getElementById('submitBtn');
|
|
1117
|
+
if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
|
|
1118
|
+
var skipBtn = document.getElementById('skipBtn');
|
|
1119
|
+
if (skipBtn) {{ skipBtn.disabled = true; }}
|
|
1120
|
+
return true;
|
|
1121
|
+
}}
|
|
1122
|
+
{extra_js}
|
|
1123
|
+
</script>
|
|
1124
|
+
"""
|
|
1125
|
+
|
|
1126
|
+
if not is_setup:
|
|
1127
|
+
form_html += f"""<p>{render_button("Back", "secondary", {"onclick": "location.href='/'"})}</p>"""
|
|
1128
|
+
|
|
1129
|
+
return form_html
|
|
1130
|
+
|
|
1131
|
+
|
|
720
1132
|
def hostnames_config(shared_state):
|
|
721
1133
|
app = Bottle()
|
|
722
1134
|
add_no_cache_headers(app)
|
|
@@ -739,87 +1151,20 @@ def hostnames_config(shared_state):
|
|
|
739
1151
|
return save_hostnames(shared_state)
|
|
740
1152
|
|
|
741
1153
|
@app.post("/api/hostnames/import-url")
|
|
742
|
-
def
|
|
743
|
-
|
|
744
|
-
response.content_type = "application/json"
|
|
745
|
-
try:
|
|
746
|
-
data = request.json
|
|
747
|
-
url = data.get("url", "").strip()
|
|
748
|
-
|
|
749
|
-
if not url:
|
|
750
|
-
return {"success": False, "error": "No URL provided"}
|
|
751
|
-
|
|
752
|
-
# Validate URL
|
|
753
|
-
parsed = urlparse(url)
|
|
754
|
-
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
755
|
-
return {"success": False, "error": "Invalid URL format"}
|
|
756
|
-
|
|
757
|
-
# Fetch content
|
|
758
|
-
try:
|
|
759
|
-
resp = requests.get(url, timeout=15)
|
|
760
|
-
resp.raise_for_status()
|
|
761
|
-
content = resp.text
|
|
762
|
-
except requests.RequestException as e:
|
|
763
|
-
info(f"Failed to fetch hostnames URL: {e}")
|
|
764
|
-
return {
|
|
765
|
-
"success": False,
|
|
766
|
-
"error": "Failed to fetch URL. Check the console log for details.",
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
# Parse hostnames
|
|
770
|
-
allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
|
|
771
|
-
results = extract_kv_pairs(content, allowed_keys)
|
|
772
|
-
|
|
773
|
-
if not results:
|
|
774
|
-
return {
|
|
775
|
-
"success": False,
|
|
776
|
-
"error": "No hostnames found in the provided URL",
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
# Validate each hostname
|
|
780
|
-
valid_hostnames = {}
|
|
781
|
-
invalid_hostnames = {}
|
|
782
|
-
for shorthand, hostname in results.items():
|
|
783
|
-
domain_check = extract_valid_hostname(hostname, shorthand)
|
|
784
|
-
domain = domain_check.get("domain")
|
|
785
|
-
if domain:
|
|
786
|
-
valid_hostnames[shorthand] = domain
|
|
787
|
-
else:
|
|
788
|
-
invalid_hostnames[shorthand] = domain_check.get(
|
|
789
|
-
"message", "Invalid"
|
|
790
|
-
)
|
|
791
|
-
|
|
792
|
-
if not valid_hostnames:
|
|
793
|
-
return {
|
|
794
|
-
"success": False,
|
|
795
|
-
"error": "No valid hostnames found in the provided URL",
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
return {
|
|
799
|
-
"success": True,
|
|
800
|
-
"hostnames": valid_hostnames,
|
|
801
|
-
"errors": invalid_hostnames,
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
except Exception as e:
|
|
805
|
-
return {"success": False, "error": f"Error: {str(e)}"}
|
|
1154
|
+
def import_hostnames_route():
|
|
1155
|
+
return import_hostnames_from_url()
|
|
806
1156
|
|
|
807
1157
|
@app.get("/api/skip-login")
|
|
808
|
-
def
|
|
809
|
-
|
|
810
|
-
response.content_type = "application/json"
|
|
811
|
-
skip_db = DataBase("skip_login")
|
|
812
|
-
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
813
|
-
skipped = []
|
|
814
|
-
for site in login_required_sites:
|
|
815
|
-
if skip_db.retrieve(site):
|
|
816
|
-
skipped.append(site)
|
|
817
|
-
return {"skipped": skipped}
|
|
1158
|
+
def get_skip_login_route():
|
|
1159
|
+
return get_skip_login()
|
|
818
1160
|
|
|
819
1161
|
@app.delete("/api/skip-login/<shorthand>")
|
|
820
|
-
def
|
|
821
|
-
|
|
822
|
-
|
|
1162
|
+
def clear_skip_login_route(shorthand):
|
|
1163
|
+
return clear_skip_login(shorthand)
|
|
1164
|
+
|
|
1165
|
+
@app.post("/api/hostnames/check-credentials/<shorthand>")
|
|
1166
|
+
def check_credentials_route(shorthand):
|
|
1167
|
+
return check_credentials(shared_state, shorthand)
|
|
823
1168
|
|
|
824
1169
|
info(
|
|
825
1170
|
f'Hostnames not set. Starting web server for config at: "{shared_state.values["internal_address"]}".'
|
|
@@ -838,15 +1183,50 @@ def hostname_credentials_config(shared_state, shorthand, domain):
|
|
|
838
1183
|
|
|
839
1184
|
shorthand = shorthand.upper()
|
|
840
1185
|
|
|
1186
|
+
@app.post("/api/flaresolverr_inline")
|
|
1187
|
+
def set_flaresolverr_inline():
|
|
1188
|
+
return save_flaresolverr_url(shared_state, is_setup=False)
|
|
1189
|
+
|
|
841
1190
|
@app.get("/")
|
|
842
1191
|
def credentials_form():
|
|
843
|
-
|
|
1192
|
+
flaresolverr_url = Config("FlareSolverr").get("url")
|
|
1193
|
+
|
|
1194
|
+
is_al_missing_flaresolverr = shorthand == "AL" and not flaresolverr_url
|
|
1195
|
+
|
|
1196
|
+
flaresolverr_section = ""
|
|
1197
|
+
|
|
1198
|
+
if is_al_missing_flaresolverr:
|
|
1199
|
+
flaresolverr_section = """
|
|
1200
|
+
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 0.5rem;">
|
|
1201
|
+
<h4 style="margin-top:0; font-size:1rem; color:#856404;">⚠️ FlareSolverr Required</h4>
|
|
1202
|
+
<p style="font-size:0.875rem; margin-bottom:0.5rem; color:#856404;">
|
|
1203
|
+
This site requires FlareSolverr. Please configure it below before checking credentials.
|
|
1204
|
+
</p>
|
|
1205
|
+
<form action="/api/flaresolverr_inline" method="post" onsubmit="return handleFlareSolverrSubmit(this)">
|
|
1206
|
+
<div style="display:flex; gap:0.5rem;">
|
|
1207
|
+
<input type="text" name="url" placeholder="http://192.168.0.1:8191/v1" style="flex:1; margin-bottom:0;">
|
|
1208
|
+
<button type="submit" class="btn-secondary" id="fsSubmitBtn" style="margin-top:0;">Save URL</button>
|
|
1209
|
+
</div>
|
|
1210
|
+
</form>
|
|
1211
|
+
</div>
|
|
1212
|
+
<script>
|
|
1213
|
+
function handleFlareSolverrSubmit(form) {{
|
|
1214
|
+
var btn = document.getElementById('fsSubmitBtn');
|
|
1215
|
+
if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
|
|
1216
|
+
return true;
|
|
1217
|
+
}}
|
|
1218
|
+
</script>
|
|
1219
|
+
"""
|
|
1220
|
+
|
|
1221
|
+
disabled_attr = "disabled" if is_al_missing_flaresolverr else ""
|
|
1222
|
+
|
|
1223
|
+
credentials_inputs = f"""
|
|
844
1224
|
<span>If required register account at: <a href="https://{domain}">{domain}</a>!</span><br><br>
|
|
845
1225
|
<label for="user">Username</label>
|
|
846
|
-
<input type="text" id="user" name="user" placeholder="User" autocorrect="off"><br>
|
|
1226
|
+
<input type="text" id="user" name="user" placeholder="User" autocorrect="off" {disabled_attr}><br>
|
|
847
1227
|
|
|
848
1228
|
<label for="password">Password</label>
|
|
849
|
-
<input type="password" id="password" name="password" placeholder="Password"><br>
|
|
1229
|
+
<input type="password" id="password" name="password" placeholder="Password" {disabled_attr}><br>
|
|
850
1230
|
"""
|
|
851
1231
|
|
|
852
1232
|
form_html = f"""
|
|
@@ -873,8 +1253,9 @@ def hostname_credentials_config(shared_state, shorthand, domain):
|
|
|
873
1253
|
border-color: #c69500;
|
|
874
1254
|
}}
|
|
875
1255
|
</style>
|
|
1256
|
+
{flaresolverr_section}
|
|
876
1257
|
<form id="credentialsForm" action="/api/credentials/{shorthand}" method="post" onsubmit="return handleSubmit(this)">
|
|
877
|
-
{
|
|
1258
|
+
{credentials_inputs}
|
|
878
1259
|
<div class="button-row">
|
|
879
1260
|
{render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
|
|
880
1261
|
<button type="button" class="btn-warning" id="skipBtn" onclick="skipLogin()">Skip for now</button>
|
|
@@ -885,7 +1266,14 @@ def hostname_credentials_config(shared_state, shorthand, domain):
|
|
|
885
1266
|
</p>
|
|
886
1267
|
<script>
|
|
887
1268
|
var formSubmitted = false;
|
|
1269
|
+
var isAlMissingFlaresolverr = {"true" if is_al_missing_flaresolverr else "false"};
|
|
1270
|
+
|
|
888
1271
|
function handleSubmit(form) {{
|
|
1272
|
+
if (isAlMissingFlaresolverr) {{
|
|
1273
|
+
showModal('FlareSolverr Required', 'You must configure FlareSolverr below or skip login for this site.');
|
|
1274
|
+
return false;
|
|
1275
|
+
}}
|
|
1276
|
+
|
|
889
1277
|
if (formSubmitted) return false;
|
|
890
1278
|
formSubmitted = true;
|
|
891
1279
|
var btn = document.getElementById('submitBtn');
|
|
@@ -1023,85 +1411,9 @@ def flaresolverr_config(shared_state):
|
|
|
1023
1411
|
|
|
1024
1412
|
@app.get("/")
|
|
1025
1413
|
def url_form():
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
<label for="url">FlareSolverr URL</label>
|
|
1030
|
-
<input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1"><br>
|
|
1031
|
-
"""
|
|
1032
|
-
form_html = f"""
|
|
1033
|
-
<style>
|
|
1034
|
-
.button-row {{
|
|
1035
|
-
display: flex;
|
|
1036
|
-
gap: 0.75rem;
|
|
1037
|
-
justify-content: center;
|
|
1038
|
-
flex-wrap: wrap;
|
|
1039
|
-
margin-top: 1rem;
|
|
1040
|
-
}}
|
|
1041
|
-
.btn-warning {{
|
|
1042
|
-
background-color: #ffc107;
|
|
1043
|
-
color: #212529;
|
|
1044
|
-
border: 1.5px solid #d39e00;
|
|
1045
|
-
padding: 0.5rem 1rem;
|
|
1046
|
-
font-size: 1rem;
|
|
1047
|
-
border-radius: 0.5rem;
|
|
1048
|
-
font-weight: 500;
|
|
1049
|
-
cursor: pointer;
|
|
1050
|
-
}}
|
|
1051
|
-
.btn-warning:hover {{
|
|
1052
|
-
background-color: #e0a800;
|
|
1053
|
-
border-color: #c69500;
|
|
1054
|
-
}}
|
|
1055
|
-
</style>
|
|
1056
|
-
<form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
|
|
1057
|
-
{form_content}
|
|
1058
|
-
<div class="button-row">
|
|
1059
|
-
{render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
|
|
1060
|
-
<button type="button" class="btn-warning" id="skipBtn" onclick="skipFlaresolverr()">Skip for now</button>
|
|
1061
|
-
</div>
|
|
1062
|
-
</form>
|
|
1063
|
-
<p style="font-size:0.875rem; color:var(--secondary, #6c757d); margin-top:1rem;">
|
|
1064
|
-
Skipping will allow Quasarr to start, but some sites (like AL) won't work without FlareSolverr.
|
|
1065
|
-
</p>
|
|
1066
|
-
<script>
|
|
1067
|
-
var formSubmitted = false;
|
|
1068
|
-
function handleSubmit(form) {{
|
|
1069
|
-
if (formSubmitted) return false;
|
|
1070
|
-
formSubmitted = true;
|
|
1071
|
-
var btn = document.getElementById('submitBtn');
|
|
1072
|
-
if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
|
|
1073
|
-
document.getElementById('skipBtn').disabled = true;
|
|
1074
|
-
return true;
|
|
1075
|
-
}}
|
|
1076
|
-
function skipFlaresolverr() {{
|
|
1077
|
-
if (formSubmitted) return;
|
|
1078
|
-
formSubmitted = true;
|
|
1079
|
-
var skipBtn = document.getElementById('skipBtn');
|
|
1080
|
-
var submitBtn = document.getElementById('submitBtn');
|
|
1081
|
-
if (skipBtn) {{ skipBtn.disabled = true; skipBtn.textContent = 'Skipping...'; }}
|
|
1082
|
-
if (submitBtn) {{ submitBtn.disabled = true; }}
|
|
1083
|
-
|
|
1084
|
-
fetch('/api/flaresolverr/skip', {{ method: 'POST' }})
|
|
1085
|
-
.then(response => {{
|
|
1086
|
-
if (response.ok) {{
|
|
1087
|
-
window.location.href = '/skip-success';
|
|
1088
|
-
}} else {{
|
|
1089
|
-
showModal('Error', 'Failed to skip FlareSolverr setup');
|
|
1090
|
-
formSubmitted = false;
|
|
1091
|
-
if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
|
|
1092
|
-
if (submitBtn) {{ submitBtn.disabled = false; }}
|
|
1093
|
-
}}
|
|
1094
|
-
}})
|
|
1095
|
-
.catch(error => {{
|
|
1096
|
-
showModal('Error', 'Error: ' + error.message);
|
|
1097
|
-
formSubmitted = false;
|
|
1098
|
-
if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
|
|
1099
|
-
if (submitBtn) {{ submitBtn.disabled = false; }}
|
|
1100
|
-
}});
|
|
1101
|
-
}}
|
|
1102
|
-
</script>
|
|
1103
|
-
"""
|
|
1104
|
-
return render_form("Set FlareSolverr URL", form_html)
|
|
1414
|
+
return render_form(
|
|
1415
|
+
"Set FlareSolverr URL", flaresolverr_form_html(shared_state, is_setup=True)
|
|
1416
|
+
)
|
|
1105
1417
|
|
|
1106
1418
|
@app.get("/skip-success")
|
|
1107
1419
|
def skip_success():
|
|
@@ -1121,38 +1433,7 @@ def flaresolverr_config(shared_state):
|
|
|
1121
1433
|
|
|
1122
1434
|
@app.post("/api/flaresolverr")
|
|
1123
1435
|
def set_flaresolverr_url():
|
|
1124
|
-
|
|
1125
|
-
config = Config("FlareSolverr")
|
|
1126
|
-
|
|
1127
|
-
if not url.startswith("http://") and not url.startswith("https://"):
|
|
1128
|
-
url = "http://" + url
|
|
1129
|
-
|
|
1130
|
-
if url:
|
|
1131
|
-
try:
|
|
1132
|
-
headers = {"Content-Type": "application/json"}
|
|
1133
|
-
data = {
|
|
1134
|
-
"cmd": "request.get",
|
|
1135
|
-
"url": "http://www.google.com/",
|
|
1136
|
-
"maxTimeout": 30000,
|
|
1137
|
-
}
|
|
1138
|
-
resp = requests.post(url, headers=headers, json=data, timeout=30)
|
|
1139
|
-
if resp.status_code == 200:
|
|
1140
|
-
config.save("url", url)
|
|
1141
|
-
# Clear skip preference since we now have a working URL
|
|
1142
|
-
DataBase("skip_flaresolverr").delete("skipped")
|
|
1143
|
-
print(f'Using Flaresolverr URL: "{url}"')
|
|
1144
|
-
quasarr.providers.web_server.temp_server_success = True
|
|
1145
|
-
return render_reconnect_success(
|
|
1146
|
-
"FlareSolverr URL saved successfully!"
|
|
1147
|
-
)
|
|
1148
|
-
except requests.RequestException:
|
|
1149
|
-
pass
|
|
1150
|
-
|
|
1151
|
-
# on failure, clear any existing value and notify user
|
|
1152
|
-
config.save("url", "")
|
|
1153
|
-
return render_fail(
|
|
1154
|
-
"Could not reach FlareSolverr at that URL (expected HTTP 200)."
|
|
1155
|
-
)
|
|
1436
|
+
return save_flaresolverr_url(shared_state, is_setup=True)
|
|
1156
1437
|
|
|
1157
1438
|
info(
|
|
1158
1439
|
'"flaresolverr" URL is required for some sites (like AL). '
|