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