quasarr 2.4.7__py3-none-any.whl → 2.4.9__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.
- 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.7.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
- quasarr-2.4.9.dist-info/RECORD +81 -0
- quasarr-2.4.7.dist-info/RECORD +0 -81
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/api/config/__init__.py
CHANGED
|
@@ -12,33 +12,49 @@ from urllib.parse import urlparse
|
|
|
12
12
|
import requests
|
|
13
13
|
from bottle import request, response
|
|
14
14
|
|
|
15
|
-
from quasarr.providers.html_templates import
|
|
15
|
+
from quasarr.providers.html_templates import render_button, render_fail, render_form
|
|
16
16
|
from quasarr.providers.log import info
|
|
17
17
|
from quasarr.providers.shared_state import extract_valid_hostname
|
|
18
|
-
from quasarr.providers.utils import
|
|
18
|
+
from quasarr.providers.utils import (
|
|
19
|
+
check_flaresolverr,
|
|
20
|
+
extract_allowed_keys,
|
|
21
|
+
extract_kv_pairs,
|
|
22
|
+
)
|
|
19
23
|
from quasarr.storage.config import Config
|
|
20
|
-
from quasarr.storage.setup import
|
|
24
|
+
from quasarr.storage.setup import (
|
|
25
|
+
hostname_form_html,
|
|
26
|
+
render_reconnect_success,
|
|
27
|
+
save_hostnames,
|
|
28
|
+
)
|
|
21
29
|
from quasarr.storage.sqlite_database import DataBase
|
|
22
30
|
|
|
23
31
|
|
|
24
32
|
def setup_config(app, shared_state):
|
|
25
33
|
@app.get("/api/hostname-issues")
|
|
26
34
|
def get_hostname_issues_api():
|
|
27
|
-
response.content_type =
|
|
35
|
+
response.content_type = "application/json"
|
|
28
36
|
from quasarr.providers.hostname_issues import get_all_hostname_issues
|
|
37
|
+
|
|
29
38
|
return {"issues": get_all_hostname_issues()}
|
|
30
39
|
|
|
31
|
-
@app.get(
|
|
40
|
+
@app.get("/hostnames")
|
|
32
41
|
def hostnames_ui():
|
|
33
42
|
message = """<p>
|
|
34
43
|
At least one hostname must be kept.
|
|
35
44
|
</p>"""
|
|
36
|
-
back_button = f
|
|
45
|
+
back_button = f"""<p>
|
|
37
46
|
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
38
|
-
</p>
|
|
39
|
-
return render_form(
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
</p>"""
|
|
48
|
+
return render_form(
|
|
49
|
+
"Hostnames",
|
|
50
|
+
hostname_form_html(
|
|
51
|
+
shared_state,
|
|
52
|
+
message,
|
|
53
|
+
show_restart_button=True,
|
|
54
|
+
show_skip_management=True,
|
|
55
|
+
)
|
|
56
|
+
+ back_button,
|
|
57
|
+
)
|
|
42
58
|
|
|
43
59
|
@app.post("/api/hostnames")
|
|
44
60
|
def hostnames_api():
|
|
@@ -47,10 +63,10 @@ def setup_config(app, shared_state):
|
|
|
47
63
|
@app.post("/api/hostnames/import-url")
|
|
48
64
|
def import_hostnames_from_url():
|
|
49
65
|
"""Fetch URL and parse hostnames, return JSON for JS to populate fields."""
|
|
50
|
-
response.content_type =
|
|
66
|
+
response.content_type = "application/json"
|
|
51
67
|
try:
|
|
52
68
|
data = request.json
|
|
53
|
-
url = data.get(
|
|
69
|
+
url = data.get("url", "").strip()
|
|
54
70
|
|
|
55
71
|
if not url:
|
|
56
72
|
return {"success": False, "error": "No URL provided"}
|
|
@@ -67,33 +83,44 @@ def setup_config(app, shared_state):
|
|
|
67
83
|
content = resp.text
|
|
68
84
|
except requests.RequestException as e:
|
|
69
85
|
info(f"Failed to fetch hostnames URL: {e}")
|
|
70
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
"success": False,
|
|
88
|
+
"error": "Failed to fetch URL. Check the console log for details.",
|
|
89
|
+
}
|
|
71
90
|
|
|
72
91
|
# Parse hostnames
|
|
73
|
-
allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG,
|
|
92
|
+
allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
|
|
74
93
|
results = extract_kv_pairs(content, allowed_keys)
|
|
75
94
|
|
|
76
95
|
if not results:
|
|
77
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": "No hostnames found in the provided URL",
|
|
99
|
+
}
|
|
78
100
|
|
|
79
101
|
# Validate each hostname
|
|
80
102
|
valid_hostnames = {}
|
|
81
103
|
invalid_hostnames = {}
|
|
82
104
|
for shorthand, hostname in results.items():
|
|
83
105
|
domain_check = extract_valid_hostname(hostname, shorthand)
|
|
84
|
-
domain = domain_check.get(
|
|
106
|
+
domain = domain_check.get("domain")
|
|
85
107
|
if domain:
|
|
86
108
|
valid_hostnames[shorthand] = domain
|
|
87
109
|
else:
|
|
88
|
-
invalid_hostnames[shorthand] = domain_check.get(
|
|
110
|
+
invalid_hostnames[shorthand] = domain_check.get(
|
|
111
|
+
"message", "Invalid"
|
|
112
|
+
)
|
|
89
113
|
|
|
90
114
|
if not valid_hostnames:
|
|
91
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
"success": False,
|
|
117
|
+
"error": "No valid hostnames found in the provided URL",
|
|
118
|
+
}
|
|
92
119
|
|
|
93
120
|
return {
|
|
94
121
|
"success": True,
|
|
95
122
|
"hostnames": valid_hostnames,
|
|
96
|
-
"errors": invalid_hostnames
|
|
123
|
+
"errors": invalid_hostnames,
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
except Exception as e:
|
|
@@ -102,9 +129,9 @@ def setup_config(app, shared_state):
|
|
|
102
129
|
@app.get("/api/skip-login")
|
|
103
130
|
def get_skip_login():
|
|
104
131
|
"""Return list of hostnames with skipped login."""
|
|
105
|
-
response.content_type =
|
|
132
|
+
response.content_type = "application/json"
|
|
106
133
|
skip_db = DataBase("skip_login")
|
|
107
|
-
login_required_sites = [
|
|
134
|
+
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
108
135
|
skipped = []
|
|
109
136
|
for site in login_required_sites:
|
|
110
137
|
if skip_db.retrieve(site):
|
|
@@ -114,9 +141,9 @@ def setup_config(app, shared_state):
|
|
|
114
141
|
@app.delete("/api/skip-login/<shorthand>")
|
|
115
142
|
def clear_skip_login(shorthand):
|
|
116
143
|
"""Clear skip login preference for a hostname."""
|
|
117
|
-
response.content_type =
|
|
144
|
+
response.content_type = "application/json"
|
|
118
145
|
shorthand = shorthand.lower()
|
|
119
|
-
login_required_sites = [
|
|
146
|
+
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
120
147
|
if shorthand not in login_required_sites:
|
|
121
148
|
return {"success": False, "error": f"Invalid shorthand: {shorthand}"}
|
|
122
149
|
|
|
@@ -125,23 +152,23 @@ def setup_config(app, shared_state):
|
|
|
125
152
|
info(f'Skip login preference cleared for "{shorthand.upper()}"')
|
|
126
153
|
return {"success": True}
|
|
127
154
|
|
|
128
|
-
@app.get(
|
|
155
|
+
@app.get("/flaresolverr")
|
|
129
156
|
def flaresolverr_ui():
|
|
130
157
|
"""Web UI page for configuring FlareSolverr."""
|
|
131
158
|
skip_db = DataBase("skip_flaresolverr")
|
|
132
159
|
is_skipped = skip_db.retrieve("skipped")
|
|
133
|
-
current_url = Config(
|
|
160
|
+
current_url = Config("FlareSolverr").get("url") or ""
|
|
134
161
|
|
|
135
162
|
skip_indicator = ""
|
|
136
163
|
if is_skipped:
|
|
137
|
-
skip_indicator =
|
|
164
|
+
skip_indicator = """
|
|
138
165
|
<div class="skip-indicator" style="margin-bottom:1rem; padding:0.75rem; background:var(--code-bg, #f8f9fa); border-radius:0.25rem; font-size:0.875rem;">
|
|
139
166
|
<span style="color:#dc3545;">⚠️ FlareSolverr setup was skipped</span>
|
|
140
167
|
<p style="margin:0.5rem 0 0 0; font-size:0.75rem; color:var(--secondary, #6c757d);">
|
|
141
168
|
Some sites (like AL) won't work until FlareSolverr is configured.
|
|
142
169
|
</p>
|
|
143
170
|
</div>
|
|
144
|
-
|
|
171
|
+
"""
|
|
145
172
|
|
|
146
173
|
form_content = f'''
|
|
147
174
|
{skip_indicator}
|
|
@@ -151,7 +178,7 @@ def setup_config(app, shared_state):
|
|
|
151
178
|
<input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1" value="{current_url}"><br>
|
|
152
179
|
'''
|
|
153
180
|
|
|
154
|
-
form_html = f
|
|
181
|
+
form_html = f"""
|
|
155
182
|
<form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
|
|
156
183
|
{form_content}
|
|
157
184
|
{render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
|
|
@@ -245,13 +272,13 @@ def setup_config(app, shared_state):
|
|
|
245
272
|
attempt();
|
|
246
273
|
}}
|
|
247
274
|
</script>
|
|
248
|
-
|
|
275
|
+
"""
|
|
249
276
|
return render_form("FlareSolverr", form_html)
|
|
250
277
|
|
|
251
|
-
@app.post(
|
|
278
|
+
@app.post("/api/flaresolverr")
|
|
252
279
|
def set_flaresolverr_url():
|
|
253
280
|
"""Save FlareSolverr URL from web UI."""
|
|
254
|
-
url = request.forms.get(
|
|
281
|
+
url = request.forms.get("url", "").strip()
|
|
255
282
|
config = Config("FlareSolverr")
|
|
256
283
|
|
|
257
284
|
if not url:
|
|
@@ -262,14 +289,16 @@ def setup_config(app, shared_state):
|
|
|
262
289
|
|
|
263
290
|
# Validate URL format
|
|
264
291
|
if not re.search(r"/v\d+$", url):
|
|
265
|
-
return render_fail(
|
|
292
|
+
return render_fail(
|
|
293
|
+
"FlareSolverr URL must end with /v1 (or similar version path)."
|
|
294
|
+
)
|
|
266
295
|
|
|
267
296
|
try:
|
|
268
297
|
headers = {"Content-Type": "application/json"}
|
|
269
298
|
data = {
|
|
270
299
|
"cmd": "request.get",
|
|
271
300
|
"url": "http://www.google.com/",
|
|
272
|
-
"maxTimeout": 30000
|
|
301
|
+
"maxTimeout": 30000,
|
|
273
302
|
}
|
|
274
303
|
resp = requests.post(url, headers=headers, json=data, timeout=30)
|
|
275
304
|
if resp.status_code == 200:
|
|
@@ -285,46 +314,47 @@ def setup_config(app, shared_state):
|
|
|
285
314
|
shared_state.update("user_agent", solution_ua)
|
|
286
315
|
info(f'FlareSolverr URL configured: "{url}"')
|
|
287
316
|
return render_reconnect_success(
|
|
288
|
-
"FlareSolverr URL saved successfully! A restart is recommended."
|
|
317
|
+
"FlareSolverr URL saved successfully! A restart is recommended."
|
|
318
|
+
)
|
|
289
319
|
else:
|
|
290
|
-
return render_fail(
|
|
320
|
+
return render_fail(
|
|
321
|
+
f"FlareSolverr returned unexpected status: {json_data.get('status')}"
|
|
322
|
+
)
|
|
291
323
|
except requests.RequestException:
|
|
292
324
|
return render_fail(f"Could not reach FlareSolverr!")
|
|
293
325
|
|
|
294
|
-
return render_fail(
|
|
326
|
+
return render_fail(
|
|
327
|
+
"Could not reach FlareSolverr at that URL (expected HTTP 200)."
|
|
328
|
+
)
|
|
295
329
|
|
|
296
330
|
@app.get("/api/flaresolverr/status")
|
|
297
331
|
def get_flaresolverr_status():
|
|
298
332
|
"""Return FlareSolverr configuration status."""
|
|
299
|
-
response.content_type =
|
|
333
|
+
response.content_type = "application/json"
|
|
300
334
|
skip_db = DataBase("skip_flaresolverr")
|
|
301
335
|
is_skipped = bool(skip_db.retrieve("skipped"))
|
|
302
|
-
current_url = Config(
|
|
336
|
+
current_url = Config("FlareSolverr").get("url") or ""
|
|
303
337
|
|
|
304
338
|
# Test connection if URL is set
|
|
305
339
|
is_working = False
|
|
306
340
|
if current_url and not is_skipped:
|
|
307
341
|
is_working = check_flaresolverr(shared_state, current_url)
|
|
308
342
|
|
|
309
|
-
return {
|
|
310
|
-
"skipped": is_skipped,
|
|
311
|
-
"url": current_url,
|
|
312
|
-
"working": is_working
|
|
313
|
-
}
|
|
343
|
+
return {"skipped": is_skipped, "url": current_url, "working": is_working}
|
|
314
344
|
|
|
315
345
|
@app.delete("/api/skip-flaresolverr")
|
|
316
346
|
def clear_skip_flaresolverr():
|
|
317
347
|
"""Clear skip FlareSolverr preference."""
|
|
318
|
-
response.content_type =
|
|
348
|
+
response.content_type = "application/json"
|
|
319
349
|
skip_db = DataBase("skip_flaresolverr")
|
|
320
350
|
skip_db.delete("skipped")
|
|
321
|
-
info(
|
|
351
|
+
info("Skip FlareSolverr preference cleared")
|
|
322
352
|
return {"success": True}
|
|
323
353
|
|
|
324
354
|
@app.post("/api/restart")
|
|
325
355
|
def restart_quasarr():
|
|
326
356
|
"""Restart Quasarr. In Docker with the restart loop, exit(0) triggers restart."""
|
|
327
|
-
response.content_type =
|
|
357
|
+
response.content_type = "application/json"
|
|
328
358
|
info("Restart requested via web UI")
|
|
329
359
|
|
|
330
360
|
def delayed_exit():
|