quasarr 2.4.11__py3-none-any.whl → 2.6.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 +30 -35
- quasarr/api/__init__.py +23 -15
- quasarr/api/arr/__init__.py +15 -6
- quasarr/api/captcha/__init__.py +2 -9
- quasarr/api/config/__init__.py +31 -168
- quasarr/api/jdownloader/__init__.py +232 -0
- quasarr/api/packages/__init__.py +2 -12
- quasarr/downloads/__init__.py +2 -0
- quasarr/downloads/sources/hs.py +131 -0
- quasarr/providers/html_templates.py +14 -3
- quasarr/providers/sessions/al.py +4 -0
- quasarr/providers/shared_state.py +17 -17
- quasarr/providers/version.py +1 -1
- quasarr/search/__init__.py +90 -15
- quasarr/search/sources/al.py +17 -13
- quasarr/search/sources/by.py +4 -1
- quasarr/search/sources/dd.py +16 -4
- quasarr/search/sources/dl.py +13 -1
- quasarr/search/sources/hs.py +515 -0
- quasarr/search/sources/mb.py +1 -7
- quasarr/search/sources/nx.py +4 -1
- quasarr/search/sources/wd.py +4 -1
- quasarr/search/sources/wx.py +10 -8
- quasarr/storage/config.py +1 -0
- quasarr/storage/setup.py +564 -266
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/METADATA +1 -1
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/RECORD +30 -27
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/WHEEL +0 -0
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/licenses/LICENSE +0 -0
quasarr/__init__.py
CHANGED
|
@@ -386,48 +386,43 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
|
|
|
386
386
|
try:
|
|
387
387
|
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
while True:
|
|
390
|
+
shared_state.set_device_from_config()
|
|
390
391
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if not connection_established:
|
|
395
|
-
i = 0
|
|
396
|
-
while i < 10:
|
|
397
|
-
i += 1
|
|
392
|
+
device = shared_state.get_device()
|
|
393
|
+
|
|
394
|
+
try:
|
|
398
395
|
info(
|
|
399
|
-
f'Connection
|
|
396
|
+
f'Connection to JDownloader successful. Device name: "{device.name}"'
|
|
400
397
|
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
shared_state.get_device() and shared_state.get_device().name
|
|
405
|
-
)
|
|
406
|
-
if connection_established:
|
|
407
|
-
break
|
|
398
|
+
except Exception as e:
|
|
399
|
+
info(f"Error connecting to JDownloader: {e}! Stopping Quasarr...")
|
|
400
|
+
sys.exit(1)
|
|
408
401
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
except Exception as e:
|
|
414
|
-
info(f"Error connecting to JDownloader: {e}! Stopping Quasarr!")
|
|
415
|
-
sys.exit(1)
|
|
402
|
+
try:
|
|
403
|
+
shared_state.set_device_settings()
|
|
404
|
+
except Exception as e:
|
|
405
|
+
print(f"Error checking settings: {e}")
|
|
416
406
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
407
|
+
try:
|
|
408
|
+
shared_state.update_jdownloader()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
print(f"Error updating JDownloader: {e}")
|
|
421
411
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
412
|
+
try:
|
|
413
|
+
shared_state.start_downloads()
|
|
414
|
+
except Exception as e:
|
|
415
|
+
print(f"Error starting downloads: {e}")
|
|
426
416
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
417
|
+
while True:
|
|
418
|
+
time.sleep(300)
|
|
419
|
+
device_state = shared_state.check_device(
|
|
420
|
+
shared_state.values.get("device")
|
|
421
|
+
)
|
|
422
|
+
if not device_state:
|
|
423
|
+
info("Lost connection to JDownloader. Reconnecting...")
|
|
424
|
+
shared_state.update("device", False)
|
|
425
|
+
break
|
|
431
426
|
|
|
432
427
|
except KeyboardInterrupt:
|
|
433
428
|
pass
|
quasarr/api/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ import quasarr.providers.html_images as images
|
|
|
8
8
|
from quasarr.api.arr import setup_arr_routes
|
|
9
9
|
from quasarr.api.captcha import setup_captcha_routes
|
|
10
10
|
from quasarr.api.config import setup_config
|
|
11
|
+
from quasarr.api.jdownloader import get_jdownloader_modal_script, get_jdownloader_status
|
|
11
12
|
from quasarr.api.packages import setup_packages_routes
|
|
12
13
|
from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
13
14
|
from quasarr.api.statistics import setup_statistics
|
|
@@ -49,12 +50,9 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
49
50
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
50
51
|
api_key = Config("API").get("key")
|
|
51
52
|
|
|
52
|
-
# Get
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
jd_connected = device is not None and device is not False
|
|
56
|
-
except:
|
|
57
|
-
jd_connected = False
|
|
53
|
+
# Get JDownloader status and modal script
|
|
54
|
+
jd_status = get_jdownloader_status(shared_state)
|
|
55
|
+
jd_modal_script = get_jdownloader_modal_script()
|
|
58
56
|
|
|
59
57
|
# Calculate hostname status
|
|
60
58
|
hostnames_config = Config("Hostnames")
|
|
@@ -126,10 +124,14 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
126
124
|
# Status bars
|
|
127
125
|
status_bars = f"""
|
|
128
126
|
<div class="status-bar">
|
|
129
|
-
<span class="status-pill {"
|
|
130
|
-
|
|
127
|
+
<span class="status-pill {jd_status['status_class']}"
|
|
128
|
+
onclick="openJDownloaderModal()"
|
|
129
|
+
title="Click to configure JDownloader">
|
|
130
|
+
{jd_status['status_text']}
|
|
131
131
|
</span>
|
|
132
|
-
<span class="status-pill {hostname_status_class}"
|
|
132
|
+
<span class="status-pill {hostname_status_class}"
|
|
133
|
+
onclick="location.href='/hostnames'"
|
|
134
|
+
title="Click to configure Hostnames">
|
|
133
135
|
{hostname_status_emoji} {hostname_status_text}
|
|
134
136
|
</span>
|
|
135
137
|
</div>
|
|
@@ -209,6 +211,11 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
209
211
|
padding: 8px 16px;
|
|
210
212
|
border-radius: 0.5rem;
|
|
211
213
|
font-weight: 500;
|
|
214
|
+
transition: transform 0.1s ease;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
}}
|
|
217
|
+
.status-pill:hover {{
|
|
218
|
+
transform: scale(1.05);
|
|
212
219
|
}}
|
|
213
220
|
.status-pill.success {{
|
|
214
221
|
background: var(--status-success-bg, #e8f5e9);
|
|
@@ -367,15 +374,15 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
367
374
|
/* Dark mode */
|
|
368
375
|
@media (prefers-color-scheme: dark) {{
|
|
369
376
|
:root {{
|
|
370
|
-
--status-success-bg: #
|
|
371
|
-
--status-success-color: #
|
|
372
|
-
--status-success-border: #
|
|
377
|
+
--status-success-bg: #1c4532;
|
|
378
|
+
--status-success-color: #68d391;
|
|
379
|
+
--status-success-border: #276749;
|
|
373
380
|
--status-warning-bg: #3d3520;
|
|
374
381
|
--status-warning-color: #ffb74d;
|
|
375
382
|
--status-warning-border: #d69e2e;
|
|
376
|
-
--status-error-bg: #
|
|
377
|
-
--status-error-color: #
|
|
378
|
-
--status-error-border: #
|
|
383
|
+
--status-error-bg: #3d2d2d;
|
|
384
|
+
--status-error-color: #fc8181;
|
|
385
|
+
--status-error-border: #c53030;
|
|
379
386
|
--alert-warning-bg: #3d3520;
|
|
380
387
|
--alert-warning-border: #d69e2e;
|
|
381
388
|
--card-bg: #2d3748;
|
|
@@ -478,6 +485,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
478
485
|
);
|
|
479
486
|
}}
|
|
480
487
|
</script>
|
|
488
|
+
{jd_modal_script}
|
|
481
489
|
"""
|
|
482
490
|
# Add logout link for form auth
|
|
483
491
|
logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ""
|
quasarr/api/arr/__init__.py
CHANGED
|
@@ -56,6 +56,16 @@ def parse_payload(payload_str):
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def setup_arr_routes(app):
|
|
59
|
+
def check_user_agent():
|
|
60
|
+
user_agent = request.headers.get("User-Agent") or ""
|
|
61
|
+
if not any(
|
|
62
|
+
tool in user_agent.lower() for tool in ["radarr", "sonarr", "lazylibrarian"]
|
|
63
|
+
):
|
|
64
|
+
msg = f"Unsupported User-Agent: {user_agent}. Quasarr as a compatibility layer must be called by Radarr, Sonarr or LazyLibrarian directly."
|
|
65
|
+
info(msg)
|
|
66
|
+
abort(406, msg)
|
|
67
|
+
return user_agent
|
|
68
|
+
|
|
59
69
|
@app.get("/download/")
|
|
60
70
|
def fake_nzb_file():
|
|
61
71
|
payload = request.query.payload
|
|
@@ -75,6 +85,7 @@ def setup_arr_routes(app):
|
|
|
75
85
|
@app.post("/api")
|
|
76
86
|
@require_api_key
|
|
77
87
|
def download_fake_nzb_file():
|
|
88
|
+
request_from = check_user_agent()
|
|
78
89
|
downloads = request.files.getall("name")
|
|
79
90
|
nzo_ids = [] # naming structure for package IDs expected in newznab
|
|
80
91
|
|
|
@@ -97,7 +108,6 @@ def setup_arr_routes(app):
|
|
|
97
108
|
source_key = root.find(".//file").attrib.get("source_key") or None
|
|
98
109
|
|
|
99
110
|
info(f'Attempting download for "{title}"')
|
|
100
|
-
request_from = request.headers.get("User-Agent")
|
|
101
111
|
downloaded = download(
|
|
102
112
|
shared_state,
|
|
103
113
|
request_from,
|
|
@@ -128,6 +138,8 @@ def setup_arr_routes(app):
|
|
|
128
138
|
@app.get("/api/<mirror>")
|
|
129
139
|
@require_api_key
|
|
130
140
|
def quasarr_api(mirror=None):
|
|
141
|
+
request_from = check_user_agent()
|
|
142
|
+
|
|
131
143
|
api_type = (
|
|
132
144
|
"arr_download_client"
|
|
133
145
|
if request.query.mode
|
|
@@ -198,7 +210,6 @@ def setup_arr_routes(app):
|
|
|
198
210
|
|
|
199
211
|
nzo_ids = []
|
|
200
212
|
info(f'Attempting download for "{parsed_payload["title"]}"')
|
|
201
|
-
request_from = "lazylibrarian"
|
|
202
213
|
|
|
203
214
|
downloaded = download(
|
|
204
215
|
shared_state,
|
|
@@ -267,8 +278,6 @@ def setup_arr_routes(app):
|
|
|
267
278
|
)
|
|
268
279
|
|
|
269
280
|
mode = request.query.t
|
|
270
|
-
request_from = request.headers.get("User-Agent")
|
|
271
|
-
|
|
272
281
|
if mode == "caps":
|
|
273
282
|
info(f"Providing indexer capability information to {request_from}")
|
|
274
283
|
return """<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -352,10 +361,10 @@ def setup_arr_routes(app):
|
|
|
352
361
|
mirror=mirror,
|
|
353
362
|
)
|
|
354
363
|
else:
|
|
355
|
-
|
|
364
|
+
# sonarr expects this but we will not support non-imdbid searches
|
|
365
|
+
debug(
|
|
356
366
|
f"Ignoring search request from {request_from} - only imdbid searches are supported"
|
|
357
367
|
)
|
|
358
|
-
releases = [] # sonarr expects this but we will not support non-imdbid searches
|
|
359
368
|
|
|
360
369
|
items = ""
|
|
361
370
|
for release in releases:
|
quasarr/api/captcha/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ import requests
|
|
|
11
11
|
from bottle import HTTPResponse, redirect, request, response
|
|
12
12
|
|
|
13
13
|
import quasarr.providers.html_images as images
|
|
14
|
+
from quasarr.api.jdownloader import get_jdownloader_disconnected_page
|
|
14
15
|
from quasarr.downloads.linkcrypters.filecrypt import DLC, get_filecrypt_links
|
|
15
16
|
from quasarr.downloads.packages import delete_package
|
|
16
17
|
from quasarr.providers import obfuscated, shared_state
|
|
@@ -46,15 +47,7 @@ def setup_captcha_routes(app):
|
|
|
46
47
|
except KeyError:
|
|
47
48
|
device = None
|
|
48
49
|
if not device:
|
|
49
|
-
return
|
|
50
|
-
<div class="status-bar">
|
|
51
|
-
<span class="status-pill error">
|
|
52
|
-
❌ JDownloader disconnected
|
|
53
|
-
</span>
|
|
54
|
-
</div>
|
|
55
|
-
<p>
|
|
56
|
-
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
57
|
-
</p>''')
|
|
50
|
+
return get_jdownloader_disconnected_page(shared_state)
|
|
58
51
|
|
|
59
52
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
60
53
|
if not protected:
|
quasarr/api/config/__init__.py
CHANGED
|
@@ -3,28 +3,27 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
import re
|
|
7
6
|
import signal
|
|
8
7
|
import threading
|
|
9
8
|
import time
|
|
10
|
-
from urllib.parse import urlparse
|
|
11
9
|
|
|
12
|
-
import requests
|
|
13
10
|
from bottle import request, response
|
|
14
11
|
|
|
15
|
-
from quasarr.providers.html_templates import render_button,
|
|
12
|
+
from quasarr.providers.html_templates import render_button, render_form
|
|
16
13
|
from quasarr.providers.log import info
|
|
17
|
-
from quasarr.providers.shared_state import extract_valid_hostname
|
|
18
|
-
from quasarr.providers.utils import (
|
|
19
|
-
check_flaresolverr,
|
|
20
|
-
extract_allowed_keys,
|
|
21
|
-
extract_kv_pairs,
|
|
22
|
-
)
|
|
23
14
|
from quasarr.storage.config import Config
|
|
24
15
|
from quasarr.storage.setup import (
|
|
16
|
+
check_credentials,
|
|
17
|
+
clear_skip_login,
|
|
18
|
+
delete_skip_flaresolverr_preference,
|
|
19
|
+
get_flaresolverr_status_data,
|
|
20
|
+
get_skip_login,
|
|
25
21
|
hostname_form_html,
|
|
26
|
-
|
|
22
|
+
import_hostnames_from_url,
|
|
23
|
+
save_flaresolverr_url,
|
|
27
24
|
save_hostnames,
|
|
25
|
+
save_jdownloader_settings,
|
|
26
|
+
verify_jdownloader_credentials,
|
|
28
27
|
)
|
|
29
28
|
from quasarr.storage.sqlite_database import DataBase
|
|
30
29
|
|
|
@@ -50,7 +49,6 @@ def setup_config(app, shared_state):
|
|
|
50
49
|
hostname_form_html(
|
|
51
50
|
shared_state,
|
|
52
51
|
message,
|
|
53
|
-
show_restart_button=True,
|
|
54
52
|
show_skip_management=True,
|
|
55
53
|
)
|
|
56
54
|
+ back_button,
|
|
@@ -60,97 +58,21 @@ def setup_config(app, shared_state):
|
|
|
60
58
|
def hostnames_api():
|
|
61
59
|
return save_hostnames(shared_state, timeout=1, first_run=False)
|
|
62
60
|
|
|
63
|
-
@app.post("/api/hostnames/
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
response.content_type = "application/json"
|
|
67
|
-
try:
|
|
68
|
-
data = request.json
|
|
69
|
-
url = data.get("url", "").strip()
|
|
70
|
-
|
|
71
|
-
if not url:
|
|
72
|
-
return {"success": False, "error": "No URL provided"}
|
|
73
|
-
|
|
74
|
-
# Validate URL
|
|
75
|
-
parsed = urlparse(url)
|
|
76
|
-
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
77
|
-
return {"success": False, "error": "Invalid URL format"}
|
|
78
|
-
|
|
79
|
-
# Fetch content
|
|
80
|
-
try:
|
|
81
|
-
resp = requests.get(url, timeout=15)
|
|
82
|
-
resp.raise_for_status()
|
|
83
|
-
content = resp.text
|
|
84
|
-
except requests.RequestException as e:
|
|
85
|
-
info(f"Failed to fetch hostnames URL: {e}")
|
|
86
|
-
return {
|
|
87
|
-
"success": False,
|
|
88
|
-
"error": "Failed to fetch URL. Check the console log for details.",
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# Parse hostnames
|
|
92
|
-
allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
|
|
93
|
-
results = extract_kv_pairs(content, allowed_keys)
|
|
94
|
-
|
|
95
|
-
if not results:
|
|
96
|
-
return {
|
|
97
|
-
"success": False,
|
|
98
|
-
"error": "No hostnames found in the provided URL",
|
|
99
|
-
}
|
|
61
|
+
@app.post("/api/hostnames/check-credentials/<shorthand>")
|
|
62
|
+
def check_credentials_api(shorthand):
|
|
63
|
+
return check_credentials(shared_state, shorthand)
|
|
100
64
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for shorthand, hostname in results.items():
|
|
105
|
-
domain_check = extract_valid_hostname(hostname, shorthand)
|
|
106
|
-
domain = domain_check.get("domain")
|
|
107
|
-
if domain:
|
|
108
|
-
valid_hostnames[shorthand] = domain
|
|
109
|
-
else:
|
|
110
|
-
invalid_hostnames[shorthand] = domain_check.get(
|
|
111
|
-
"message", "Invalid"
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
if not valid_hostnames:
|
|
115
|
-
return {
|
|
116
|
-
"success": False,
|
|
117
|
-
"error": "No valid hostnames found in the provided URL",
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
"success": True,
|
|
122
|
-
"hostnames": valid_hostnames,
|
|
123
|
-
"errors": invalid_hostnames,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
except Exception as e:
|
|
127
|
-
return {"success": False, "error": f"Error: {str(e)}"}
|
|
65
|
+
@app.post("/api/hostnames/import-url")
|
|
66
|
+
def import_hostnames_route():
|
|
67
|
+
return import_hostnames_from_url()
|
|
128
68
|
|
|
129
69
|
@app.get("/api/skip-login")
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
response.content_type = "application/json"
|
|
133
|
-
skip_db = DataBase("skip_login")
|
|
134
|
-
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
135
|
-
skipped = []
|
|
136
|
-
for site in login_required_sites:
|
|
137
|
-
if skip_db.retrieve(site):
|
|
138
|
-
skipped.append(site)
|
|
139
|
-
return {"skipped": skipped}
|
|
70
|
+
def get_skip_login_route():
|
|
71
|
+
return get_skip_login()
|
|
140
72
|
|
|
141
73
|
@app.delete("/api/skip-login/<shorthand>")
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
response.content_type = "application/json"
|
|
145
|
-
shorthand = shorthand.lower()
|
|
146
|
-
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
147
|
-
if shorthand not in login_required_sites:
|
|
148
|
-
return {"success": False, "error": f"Invalid shorthand: {shorthand}"}
|
|
149
|
-
|
|
150
|
-
skip_db = DataBase("skip_login")
|
|
151
|
-
skip_db.delete(shorthand)
|
|
152
|
-
info(f'Skip login preference cleared for "{shorthand.upper()}"')
|
|
153
|
-
return {"success": True}
|
|
74
|
+
def clear_skip_login_route(shorthand):
|
|
75
|
+
return clear_skip_login(shorthand)
|
|
154
76
|
|
|
155
77
|
@app.get("/flaresolverr")
|
|
156
78
|
def flaresolverr_ui():
|
|
@@ -183,12 +105,6 @@ def setup_config(app, shared_state):
|
|
|
183
105
|
{form_content}
|
|
184
106
|
{render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
|
|
185
107
|
</form>
|
|
186
|
-
<p style="font-size:0.875rem; color:var(--secondary, #6c757d); margin-top:1rem;">
|
|
187
|
-
A restart is recommended after configuring FlareSolverr.
|
|
188
|
-
</p>
|
|
189
|
-
<div class="section-divider" style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--divider-color, #dee2e6);">
|
|
190
|
-
{render_button("Restart Quasarr", "secondary", {"type": "button", "onclick": "confirmRestart()"})}
|
|
191
|
-
</div>
|
|
192
108
|
<p>{render_button("Back", "secondary", {"onclick": "location.href='/';"})}</p>
|
|
193
109
|
<script>
|
|
194
110
|
var formSubmitted = false;
|
|
@@ -278,78 +194,17 @@ def setup_config(app, shared_state):
|
|
|
278
194
|
@app.post("/api/flaresolverr")
|
|
279
195
|
def set_flaresolverr_url():
|
|
280
196
|
"""Save FlareSolverr URL from web UI."""
|
|
281
|
-
|
|
282
|
-
config = Config("FlareSolverr")
|
|
283
|
-
|
|
284
|
-
if not url:
|
|
285
|
-
return render_fail("Please provide a FlareSolverr URL.")
|
|
286
|
-
|
|
287
|
-
if not url.startswith("http://") and not url.startswith("https://"):
|
|
288
|
-
url = "http://" + url
|
|
289
|
-
|
|
290
|
-
# Validate URL format
|
|
291
|
-
if not re.search(r"/v\d+$", url):
|
|
292
|
-
return render_fail(
|
|
293
|
-
"FlareSolverr URL must end with /v1 (or similar version path)."
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
try:
|
|
297
|
-
headers = {"Content-Type": "application/json"}
|
|
298
|
-
data = {
|
|
299
|
-
"cmd": "request.get",
|
|
300
|
-
"url": "http://www.google.com/",
|
|
301
|
-
"maxTimeout": 30000,
|
|
302
|
-
}
|
|
303
|
-
resp = requests.post(url, headers=headers, json=data, timeout=30)
|
|
304
|
-
if resp.status_code == 200:
|
|
305
|
-
json_data = resp.json()
|
|
306
|
-
if json_data.get("status") == "ok":
|
|
307
|
-
config.save("url", url)
|
|
308
|
-
# Clear skip preference since we now have a working URL
|
|
309
|
-
DataBase("skip_flaresolverr").delete("skipped")
|
|
310
|
-
# Update user agent from FlareSolverr response
|
|
311
|
-
solution = json_data.get("solution", {})
|
|
312
|
-
solution_ua = solution.get("userAgent")
|
|
313
|
-
if solution_ua:
|
|
314
|
-
shared_state.update("user_agent", solution_ua)
|
|
315
|
-
info(f'FlareSolverr URL configured: "{url}"')
|
|
316
|
-
return render_reconnect_success(
|
|
317
|
-
"FlareSolverr URL saved successfully! A restart is recommended."
|
|
318
|
-
)
|
|
319
|
-
else:
|
|
320
|
-
return render_fail(
|
|
321
|
-
f"FlareSolverr returned unexpected status: {json_data.get('status')}"
|
|
322
|
-
)
|
|
323
|
-
except requests.RequestException:
|
|
324
|
-
return render_fail("Could not reach FlareSolverr!")
|
|
325
|
-
|
|
326
|
-
return render_fail(
|
|
327
|
-
"Could not reach FlareSolverr at that URL (expected HTTP 200)."
|
|
328
|
-
)
|
|
197
|
+
return save_flaresolverr_url(shared_state)
|
|
329
198
|
|
|
330
199
|
@app.get("/api/flaresolverr/status")
|
|
331
200
|
def get_flaresolverr_status():
|
|
332
201
|
"""Return FlareSolverr configuration status."""
|
|
333
|
-
|
|
334
|
-
skip_db = DataBase("skip_flaresolverr")
|
|
335
|
-
is_skipped = bool(skip_db.retrieve("skipped"))
|
|
336
|
-
current_url = Config("FlareSolverr").get("url") or ""
|
|
337
|
-
|
|
338
|
-
# Test connection if URL is set
|
|
339
|
-
is_working = False
|
|
340
|
-
if current_url and not is_skipped:
|
|
341
|
-
is_working = check_flaresolverr(shared_state, current_url)
|
|
342
|
-
|
|
343
|
-
return {"skipped": is_skipped, "url": current_url, "working": is_working}
|
|
202
|
+
return get_flaresolverr_status_data(shared_state)
|
|
344
203
|
|
|
345
204
|
@app.delete("/api/skip-flaresolverr")
|
|
346
205
|
def clear_skip_flaresolverr():
|
|
347
206
|
"""Clear skip FlareSolverr preference."""
|
|
348
|
-
|
|
349
|
-
skip_db = DataBase("skip_flaresolverr")
|
|
350
|
-
skip_db.delete("skipped")
|
|
351
|
-
info("Skip FlareSolverr preference cleared")
|
|
352
|
-
return {"success": True}
|
|
207
|
+
return delete_skip_flaresolverr_preference()
|
|
353
208
|
|
|
354
209
|
@app.post("/api/restart")
|
|
355
210
|
def restart_quasarr():
|
|
@@ -364,3 +219,11 @@ def setup_config(app, shared_state):
|
|
|
364
219
|
|
|
365
220
|
threading.Thread(target=delayed_exit, daemon=True).start()
|
|
366
221
|
return {"success": True, "message": "Restarting..."}
|
|
222
|
+
|
|
223
|
+
@app.post("/api/jdownloader/verify")
|
|
224
|
+
def verify_jdownloader_api():
|
|
225
|
+
return verify_jdownloader_credentials(shared_state)
|
|
226
|
+
|
|
227
|
+
@app.post("/api/jdownloader/save")
|
|
228
|
+
def save_jdownloader_api():
|
|
229
|
+
return save_jdownloader_settings(shared_state, is_setup=False)
|