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/__init__.py
CHANGED
|
@@ -15,13 +15,26 @@ import requests
|
|
|
15
15
|
import quasarr.providers.web_server
|
|
16
16
|
from quasarr.api import get_api
|
|
17
17
|
from quasarr.providers import shared_state, version
|
|
18
|
-
from quasarr.providers.log import
|
|
18
|
+
from quasarr.providers.log import debug, info
|
|
19
19
|
from quasarr.providers.notifications import send_discord_message
|
|
20
|
-
from quasarr.providers.utils import
|
|
21
|
-
|
|
20
|
+
from quasarr.providers.utils import (
|
|
21
|
+
FALLBACK_USER_AGENT,
|
|
22
|
+
Unbuffered,
|
|
23
|
+
check_flaresolverr,
|
|
24
|
+
check_ip,
|
|
25
|
+
extract_allowed_keys,
|
|
26
|
+
extract_kv_pairs,
|
|
27
|
+
is_valid_url,
|
|
28
|
+
validate_address,
|
|
29
|
+
)
|
|
22
30
|
from quasarr.storage.config import Config, get_clean_hostnames
|
|
23
|
-
from quasarr.storage.setup import
|
|
24
|
-
|
|
31
|
+
from quasarr.storage.setup import (
|
|
32
|
+
flaresolverr_config,
|
|
33
|
+
hostname_credentials_config,
|
|
34
|
+
hostnames_config,
|
|
35
|
+
jdownloader_config,
|
|
36
|
+
path_config,
|
|
37
|
+
)
|
|
25
38
|
from quasarr.storage.sqlite_database import DataBase
|
|
26
39
|
|
|
27
40
|
|
|
@@ -33,10 +46,17 @@ def run():
|
|
|
33
46
|
|
|
34
47
|
parser = argparse.ArgumentParser()
|
|
35
48
|
parser.add_argument("--port", help="Desired Port, defaults to 8080")
|
|
36
|
-
parser.add_argument(
|
|
37
|
-
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--internal_address", help="Must be provided when running in Docker"
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--external_address", help="External address for CAPTCHA notifications"
|
|
54
|
+
)
|
|
38
55
|
parser.add_argument("--discord", help="Discord Webhook URL")
|
|
39
|
-
parser.add_argument(
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--hostnames",
|
|
58
|
+
help="Public HTTP(s) Link that contains hostnames definition.",
|
|
59
|
+
)
|
|
40
60
|
arguments = parser.parse_args()
|
|
41
61
|
|
|
42
62
|
sys.stdout = Unbuffered(sys.stdout)
|
|
@@ -47,25 +67,31 @@ def run():
|
|
|
47
67
|
└────────────────────────────────────┘""")
|
|
48
68
|
|
|
49
69
|
print("\n===== Recommended Services =====")
|
|
50
|
-
print('For convenient universal premium downloads use: "https://linksnappy.com/?ref=397097"')
|
|
51
70
|
print(
|
|
52
|
-
'
|
|
71
|
+
'For convenient universal premium downloads use: "https://linksnappy.com/?ref=397097"'
|
|
72
|
+
)
|
|
73
|
+
print(
|
|
74
|
+
'Sponsors get automated CAPTCHA solutions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper"'
|
|
75
|
+
)
|
|
53
76
|
|
|
54
77
|
print("\n===== Startup Info =====")
|
|
55
|
-
port = int(
|
|
78
|
+
port = int("8080")
|
|
56
79
|
config_path = ""
|
|
57
|
-
if os.environ.get(
|
|
80
|
+
if os.environ.get("DOCKER"):
|
|
58
81
|
config_path = "/config"
|
|
59
82
|
if not arguments.internal_address:
|
|
60
83
|
print(
|
|
61
|
-
"You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://192.168.1.1:8080"
|
|
62
|
-
|
|
84
|
+
"You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://192.168.1.1:8080"
|
|
85
|
+
)
|
|
86
|
+
print(
|
|
87
|
+
"The local URL will be used by Radarr/Sonarr to connect to Quasarr"
|
|
88
|
+
)
|
|
63
89
|
print("Stopping Quasarr...")
|
|
64
90
|
sys.exit(1)
|
|
65
91
|
else:
|
|
66
92
|
if arguments.port:
|
|
67
93
|
port = int(arguments.port)
|
|
68
|
-
internal_address = f
|
|
94
|
+
internal_address = f"http://{check_ip()}:{port}"
|
|
69
95
|
|
|
70
96
|
if arguments.internal_address:
|
|
71
97
|
internal_address = arguments.internal_address
|
|
@@ -92,14 +118,13 @@ def run():
|
|
|
92
118
|
temp_file = tempfile.TemporaryFile(dir=config_path)
|
|
93
119
|
temp_file.close()
|
|
94
120
|
except Exception as e:
|
|
95
|
-
print(f'Could not access "{config_path}": {e}"'
|
|
96
|
-
f'Stopping Quasarr...')
|
|
121
|
+
print(f'Could not access "{config_path}": {e}"Stopping Quasarr...')
|
|
97
122
|
sys.exit(1)
|
|
98
123
|
|
|
99
124
|
shared_state.set_files(config_path)
|
|
100
125
|
shared_state.update("config", Config)
|
|
101
126
|
shared_state.update("database", DataBase)
|
|
102
|
-
supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG,
|
|
127
|
+
supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
|
|
103
128
|
shared_state.update("sites", [key.upper() for key in supported_hostnames])
|
|
104
129
|
# Set fallback user agent immediately so it's available while background check runs
|
|
105
130
|
shared_state.update("user_agent", FALLBACK_USER_AGENT)
|
|
@@ -117,31 +142,46 @@ def run():
|
|
|
117
142
|
print(f"Extracting hostnames from {hostnames_link}...")
|
|
118
143
|
allowed_keys = supported_hostnames
|
|
119
144
|
max_keys = len(allowed_keys)
|
|
120
|
-
shorthand_list =
|
|
121
|
-
[f'"{key}"' for key in allowed_keys[:-1]])
|
|
122
|
-
|
|
145
|
+
shorthand_list = (
|
|
146
|
+
", ".join([f'"{key}"' for key in allowed_keys[:-1]])
|
|
147
|
+
+ " and "
|
|
148
|
+
+ f'"{allowed_keys[-1]}"'
|
|
149
|
+
)
|
|
150
|
+
print(
|
|
151
|
+
f"There are up to {max_keys} hostnames currently supported: {shorthand_list}"
|
|
152
|
+
)
|
|
123
153
|
data = requests.get(hostnames_link).text
|
|
124
154
|
results = extract_kv_pairs(data, allowed_keys)
|
|
125
155
|
|
|
126
156
|
extracted_hostnames = 0
|
|
127
157
|
|
|
128
158
|
if results:
|
|
129
|
-
hostnames = Config(
|
|
159
|
+
hostnames = Config("Hostnames")
|
|
130
160
|
for shorthand, hostname in results.items():
|
|
131
|
-
domain_check = shared_state.extract_valid_hostname(
|
|
132
|
-
|
|
161
|
+
domain_check = shared_state.extract_valid_hostname(
|
|
162
|
+
hostname, shorthand
|
|
163
|
+
)
|
|
164
|
+
valid_domain = domain_check.get("domain", None)
|
|
133
165
|
if valid_domain:
|
|
134
166
|
hostnames.save(shorthand, hostname)
|
|
135
167
|
extracted_hostnames += 1
|
|
136
|
-
print(
|
|
168
|
+
print(
|
|
169
|
+
f'Hostname for "{shorthand}" successfully set to "{hostname}"'
|
|
170
|
+
)
|
|
137
171
|
else:
|
|
138
|
-
print(
|
|
172
|
+
print(
|
|
173
|
+
f'Skipping invalid hostname for "{shorthand}" ("{hostname}")'
|
|
174
|
+
)
|
|
139
175
|
if extracted_hostnames == max_keys:
|
|
140
|
-
print(f
|
|
141
|
-
print(
|
|
176
|
+
print(f"All {max_keys} hostnames successfully extracted!")
|
|
177
|
+
print(
|
|
178
|
+
"You can now remove the hostnames link from the command line / environment variable."
|
|
179
|
+
)
|
|
142
180
|
else:
|
|
143
|
-
print(
|
|
144
|
-
|
|
181
|
+
print(
|
|
182
|
+
f'No Hostnames found at "{hostnames_link}". '
|
|
183
|
+
"Ensure to pass a plain hostnames list, not html or json!"
|
|
184
|
+
)
|
|
145
185
|
else:
|
|
146
186
|
print(f'Invalid hostnames URL: "{hostnames_link}"')
|
|
147
187
|
except Exception as e:
|
|
@@ -151,42 +191,48 @@ def run():
|
|
|
151
191
|
if not hostnames:
|
|
152
192
|
hostnames_config(shared_state)
|
|
153
193
|
hostnames = get_clean_hostnames(shared_state)
|
|
154
|
-
print(
|
|
194
|
+
print(
|
|
195
|
+
f"You have [{len(hostnames)} of {len(Config._DEFAULT_CONFIG['Hostnames'])}] supported hostnames set up"
|
|
196
|
+
)
|
|
155
197
|
print(f"For efficiency it is recommended to set up as few hostnames as needed.")
|
|
156
198
|
|
|
157
199
|
# Check credentials for login-required hostnames
|
|
158
200
|
skip_login_db = DataBase("skip_login")
|
|
159
|
-
login_required_sites = [
|
|
201
|
+
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
160
202
|
|
|
161
203
|
quasarr.providers.web_server.temp_server_success = False
|
|
162
204
|
|
|
163
205
|
for site in login_required_sites:
|
|
164
|
-
hostname = Config(
|
|
206
|
+
hostname = Config("Hostnames").get(site)
|
|
165
207
|
if hostname:
|
|
166
208
|
site_config = Config(site.upper())
|
|
167
|
-
user = site_config.get(
|
|
168
|
-
password = site_config.get(
|
|
209
|
+
user = site_config.get("user")
|
|
210
|
+
password = site_config.get("password")
|
|
169
211
|
if not user or not password:
|
|
170
212
|
skip_val = skip_login_db.retrieve(site)
|
|
171
213
|
if skip_val and str(skip_val).lower() == "true":
|
|
172
214
|
info(f'"{site.upper()}" login skipped by user preference')
|
|
173
215
|
else:
|
|
174
|
-
info(
|
|
216
|
+
info(
|
|
217
|
+
f'"{site.upper()}" credentials missing. Launching setup...'
|
|
218
|
+
)
|
|
175
219
|
quasarr.providers.web_server.temp_server_success = False
|
|
176
|
-
hostname_credentials_config(
|
|
220
|
+
hostname_credentials_config(
|
|
221
|
+
shared_state, site.upper(), hostname
|
|
222
|
+
)
|
|
177
223
|
|
|
178
224
|
# Check FlareSolverr configuration
|
|
179
225
|
skip_flaresolverr_db = DataBase("skip_flaresolverr")
|
|
180
226
|
flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
|
|
181
|
-
flaresolverr_url = Config(
|
|
227
|
+
flaresolverr_url = Config("FlareSolverr").get("url")
|
|
182
228
|
|
|
183
229
|
if not flaresolverr_url and not flaresolverr_skipped:
|
|
184
230
|
flaresolverr_config(shared_state)
|
|
185
231
|
|
|
186
|
-
config = Config(
|
|
187
|
-
user = config.get(
|
|
188
|
-
password = config.get(
|
|
189
|
-
device = config.get(
|
|
232
|
+
config = Config("JDownloader")
|
|
233
|
+
user = config.get("user")
|
|
234
|
+
password = config.get("password")
|
|
235
|
+
device = config.get("device")
|
|
190
236
|
|
|
191
237
|
if not user or not password or not device:
|
|
192
238
|
jdownloader_config(shared_state)
|
|
@@ -194,7 +240,7 @@ def run():
|
|
|
194
240
|
print("\n===== Notifications =====")
|
|
195
241
|
discord_url = ""
|
|
196
242
|
if arguments.discord:
|
|
197
|
-
discord_webhook_pattern = r
|
|
243
|
+
discord_webhook_pattern = r"^https://discord\.com/api/webhooks/\d+/[\w-]+$"
|
|
198
244
|
if re.match(discord_webhook_pattern, arguments.discord):
|
|
199
245
|
shared_state.update("webhook", arguments.discord)
|
|
200
246
|
print(f"Using Discord Webhook URL for notifications.")
|
|
@@ -206,45 +252,49 @@ def run():
|
|
|
206
252
|
shared_state.update("discord", discord_url)
|
|
207
253
|
|
|
208
254
|
print("\n===== API Information =====")
|
|
209
|
-
api_key = Config(
|
|
255
|
+
api_key = Config("API").get("key")
|
|
210
256
|
if not api_key:
|
|
211
257
|
api_key = shared_state.generate_api_key()
|
|
212
258
|
|
|
213
|
-
print(
|
|
214
|
-
|
|
259
|
+
print(
|
|
260
|
+
'Setup instructions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions"'
|
|
261
|
+
)
|
|
262
|
+
print(f'URL: "{shared_state.values["internal_address"]}"')
|
|
215
263
|
print(f'API Key: "{api_key}" (without quotes)')
|
|
216
264
|
|
|
217
265
|
if external_address != internal_address:
|
|
218
266
|
print(f'External URL: "{shared_state.values["external_address"]}"')
|
|
219
267
|
|
|
220
268
|
print("\n===== Quasarr Info Log =====")
|
|
221
|
-
if os.getenv(
|
|
269
|
+
if os.getenv("DEBUG"):
|
|
222
270
|
print("===== / Debug Log =====")
|
|
223
271
|
|
|
224
272
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
225
273
|
if protected:
|
|
226
274
|
package_count = len(protected)
|
|
227
|
-
info(
|
|
228
|
-
|
|
275
|
+
info(
|
|
276
|
+
f"CAPTCHA-Solution required for {package_count} package{'s' if package_count > 1 else ''} at: "
|
|
277
|
+
f'"{shared_state.values["external_address"]}/captcha"!'
|
|
278
|
+
)
|
|
229
279
|
|
|
230
280
|
flaresolverr = multiprocessing.Process(
|
|
231
281
|
target=flaresolverr_checker,
|
|
232
282
|
args=(shared_state_dict, shared_state_lock),
|
|
233
|
-
daemon=True
|
|
283
|
+
daemon=True,
|
|
234
284
|
)
|
|
235
285
|
flaresolverr.start()
|
|
236
286
|
|
|
237
287
|
jdownloader = multiprocessing.Process(
|
|
238
288
|
target=jdownloader_connection,
|
|
239
289
|
args=(shared_state_dict, shared_state_lock),
|
|
240
|
-
daemon=True
|
|
290
|
+
daemon=True,
|
|
241
291
|
)
|
|
242
292
|
jdownloader.start()
|
|
243
293
|
|
|
244
294
|
updater = multiprocessing.Process(
|
|
245
295
|
target=update_checker,
|
|
246
296
|
args=(shared_state_dict, shared_state_lock),
|
|
247
|
-
daemon=True
|
|
297
|
+
daemon=True,
|
|
248
298
|
)
|
|
249
299
|
updater.start()
|
|
250
300
|
|
|
@@ -262,27 +312,31 @@ def flaresolverr_checker(shared_state_dict, shared_state_lock):
|
|
|
262
312
|
skip_flaresolverr_db = DataBase("skip_flaresolverr")
|
|
263
313
|
flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
|
|
264
314
|
|
|
265
|
-
flaresolverr_url = Config(
|
|
315
|
+
flaresolverr_url = Config("FlareSolverr").get("url")
|
|
266
316
|
|
|
267
317
|
# If FlareSolverr is not configured and not skipped, it means it's the first run
|
|
268
318
|
# and the user needs to be prompted via the WebUI.
|
|
269
319
|
# This background process should NOT block or prompt the user.
|
|
270
320
|
# It should only check and log the status.
|
|
271
321
|
if not flaresolverr_url and not flaresolverr_skipped:
|
|
272
|
-
info(
|
|
273
|
-
info(
|
|
322
|
+
info("FlareSolverr URL not configured. Please configure it via the WebUI.")
|
|
323
|
+
info("Some sites (AL) will not work without FlareSolverr.")
|
|
274
324
|
return # Exit the checker, it will be re-checked if user configures it later
|
|
275
325
|
|
|
276
326
|
if flaresolverr_skipped:
|
|
277
|
-
info(
|
|
278
|
-
info(
|
|
327
|
+
info("FlareSolverr setup skipped by user preference")
|
|
328
|
+
info(
|
|
329
|
+
"Some sites (AL) will not work without FlareSolverr. Configure it later in the web UI."
|
|
330
|
+
)
|
|
279
331
|
elif flaresolverr_url:
|
|
280
332
|
info(f'Checking FlareSolverr at URL: "{flaresolverr_url}"')
|
|
281
333
|
flaresolverr_check = check_flaresolverr(shared_state, flaresolverr_url)
|
|
282
334
|
if flaresolverr_check:
|
|
283
|
-
info(
|
|
335
|
+
info(
|
|
336
|
+
f'FlareSolverr connection successful. Using User-Agent: "{shared_state.values["user_agent"]}"'
|
|
337
|
+
)
|
|
284
338
|
else:
|
|
285
|
-
info(
|
|
339
|
+
info("FlareSolverr check failed - using fallback user agent")
|
|
286
340
|
# Fallback user agent is already set in main process, but we log it
|
|
287
341
|
info(f'User Agent (fallback): "{FALLBACK_USER_AGENT}"')
|
|
288
342
|
|
|
@@ -309,16 +363,18 @@ def update_checker(shared_state_dict, shared_state_lock):
|
|
|
309
363
|
info(f'Please manually check: "{link}" for more information!')
|
|
310
364
|
update_available = None
|
|
311
365
|
|
|
312
|
-
if
|
|
366
|
+
if (
|
|
367
|
+
update_available
|
|
368
|
+
and shared_state.values["last_checked_version"] != update_available
|
|
369
|
+
):
|
|
313
370
|
shared_state.update("last_checked_version", update_available)
|
|
314
371
|
info(message)
|
|
315
372
|
info(f"Please update to {update_available} as soon as possible!")
|
|
316
373
|
info(f'Release notes at: "{link}"')
|
|
317
|
-
update_available = {
|
|
318
|
-
|
|
319
|
-
"
|
|
320
|
-
|
|
321
|
-
send_discord_message(shared_state, message, "quasarr_update", details=update_available)
|
|
374
|
+
update_available = {"version": update_available, "link": link}
|
|
375
|
+
send_discord_message(
|
|
376
|
+
shared_state, message, "quasarr_update", details=update_available
|
|
377
|
+
)
|
|
322
378
|
|
|
323
379
|
# wait one hour before next check
|
|
324
380
|
time.sleep(60 * 60)
|
|
@@ -332,22 +388,30 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
|
|
|
332
388
|
|
|
333
389
|
shared_state.set_device_from_config()
|
|
334
390
|
|
|
335
|
-
connection_established =
|
|
391
|
+
connection_established = (
|
|
392
|
+
shared_state.get_device() and shared_state.get_device().name
|
|
393
|
+
)
|
|
336
394
|
if not connection_established:
|
|
337
395
|
i = 0
|
|
338
396
|
while i < 10:
|
|
339
397
|
i += 1
|
|
340
|
-
info(
|
|
398
|
+
info(
|
|
399
|
+
f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"'
|
|
400
|
+
)
|
|
341
401
|
time.sleep(60)
|
|
342
402
|
shared_state.set_device_from_config()
|
|
343
|
-
connection_established =
|
|
403
|
+
connection_established = (
|
|
404
|
+
shared_state.get_device() and shared_state.get_device().name
|
|
405
|
+
)
|
|
344
406
|
if connection_established:
|
|
345
407
|
break
|
|
346
408
|
|
|
347
409
|
try:
|
|
348
|
-
info(
|
|
410
|
+
info(
|
|
411
|
+
f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"'
|
|
412
|
+
)
|
|
349
413
|
except Exception as e:
|
|
350
|
-
info(f
|
|
414
|
+
info(f"Error connecting to JDownloader: {e}! Stopping Quasarr!")
|
|
351
415
|
sys.exit(1)
|
|
352
416
|
|
|
353
417
|
try:
|
quasarr/api/__init__.py
CHANGED
|
@@ -12,9 +12,13 @@ from quasarr.api.packages import setup_packages_routes
|
|
|
12
12
|
from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
13
13
|
from quasarr.api.statistics import setup_statistics
|
|
14
14
|
from quasarr.providers import shared_state
|
|
15
|
-
from quasarr.providers.auth import
|
|
15
|
+
from quasarr.providers.auth import add_auth_hook, add_auth_routes, show_logout_link
|
|
16
16
|
from quasarr.providers.hostname_issues import get_all_hostname_issues
|
|
17
|
-
from quasarr.providers.html_templates import
|
|
17
|
+
from quasarr.providers.html_templates import (
|
|
18
|
+
render_button,
|
|
19
|
+
render_centered_html,
|
|
20
|
+
render_success,
|
|
21
|
+
)
|
|
18
22
|
from quasarr.providers.web_server import Server
|
|
19
23
|
from quasarr.storage.config import Config
|
|
20
24
|
from quasarr.storage.sqlite_database import DataBase
|
|
@@ -27,10 +31,11 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
27
31
|
|
|
28
32
|
# Auth: routes must come first, then hook
|
|
29
33
|
add_auth_routes(app)
|
|
30
|
-
add_auth_hook(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
add_auth_hook(
|
|
35
|
+
app,
|
|
36
|
+
whitelist_prefixes=["/api", "/api/", "/sponsors_helper/", "/download/"],
|
|
37
|
+
whitelist_suffixes=[".user.js"],
|
|
38
|
+
)
|
|
34
39
|
|
|
35
40
|
setup_arr_routes(app)
|
|
36
41
|
setup_captcha_routes(app)
|
|
@@ -39,10 +44,10 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
39
44
|
setup_sponsors_helper_routes(app)
|
|
40
45
|
setup_packages_routes(app)
|
|
41
46
|
|
|
42
|
-
@app.get(
|
|
47
|
+
@app.get("/")
|
|
43
48
|
def index():
|
|
44
49
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
45
|
-
api_key = Config(
|
|
50
|
+
api_key = Config("API").get("key")
|
|
46
51
|
|
|
47
52
|
# Get quick status summary
|
|
48
53
|
try:
|
|
@@ -52,10 +57,10 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
52
57
|
jd_connected = False
|
|
53
58
|
|
|
54
59
|
# Calculate hostname status
|
|
55
|
-
hostnames_config = Config(
|
|
60
|
+
hostnames_config = Config("Hostnames")
|
|
56
61
|
skip_login_db = DataBase("skip_login")
|
|
57
62
|
hostname_issues = get_all_hostname_issues()
|
|
58
|
-
login_required_sites = [
|
|
63
|
+
login_required_sites = ["al", "dd", "dl", "nx"]
|
|
59
64
|
|
|
60
65
|
working_count = 0
|
|
61
66
|
total_count = 0
|
|
@@ -81,26 +86,30 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
81
86
|
|
|
82
87
|
# Determine status
|
|
83
88
|
if total_count == 0:
|
|
84
|
-
hostname_status_class =
|
|
85
|
-
hostname_status_emoji =
|
|
86
|
-
hostname_status_text =
|
|
89
|
+
hostname_status_class = "error"
|
|
90
|
+
hostname_status_emoji = "⚫️"
|
|
91
|
+
hostname_status_text = "No hostnames configured"
|
|
87
92
|
elif working_count == 0:
|
|
88
|
-
hostname_status_class =
|
|
89
|
-
hostname_status_emoji =
|
|
90
|
-
hostname_status_text = f
|
|
93
|
+
hostname_status_class = "error"
|
|
94
|
+
hostname_status_emoji = "🔴"
|
|
95
|
+
hostname_status_text = f"0/{total_count} hostnames operational"
|
|
91
96
|
elif working_count < total_count:
|
|
92
|
-
hostname_status_class =
|
|
93
|
-
hostname_status_emoji =
|
|
94
|
-
hostname_status_text =
|
|
97
|
+
hostname_status_class = "warning"
|
|
98
|
+
hostname_status_emoji = "🟡"
|
|
99
|
+
hostname_status_text = (
|
|
100
|
+
f"{working_count}/{total_count} hostnames operational"
|
|
101
|
+
)
|
|
95
102
|
else:
|
|
96
|
-
hostname_status_class =
|
|
97
|
-
hostname_status_emoji =
|
|
98
|
-
hostname_status_text =
|
|
103
|
+
hostname_status_class = "success"
|
|
104
|
+
hostname_status_emoji = "🟢"
|
|
105
|
+
hostname_status_text = (
|
|
106
|
+
f"{working_count}/{total_count} hostnames operational"
|
|
107
|
+
)
|
|
99
108
|
|
|
100
109
|
# CAPTCHA banner
|
|
101
110
|
captcha_hint = ""
|
|
102
111
|
if protected:
|
|
103
|
-
plural =
|
|
112
|
+
plural = "s" if len(protected) > 1 else ""
|
|
104
113
|
captcha_hint = f"""
|
|
105
114
|
<div class="alert alert-warning">
|
|
106
115
|
<span class="alert-icon">🔒</span>
|
|
@@ -109,7 +118,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
109
118
|
{"" if shared_state.values.get("helper_active") else '<br><a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" target="_blank">Sponsors get automated CAPTCHA solutions!</a>'}
|
|
110
119
|
</div>
|
|
111
120
|
<div class="alert-action">
|
|
112
|
-
{render_button(f"Solve CAPTCHA{plural}",
|
|
121
|
+
{render_button(f"Solve CAPTCHA{plural}", "primary", {"onclick": "location.href='/captcha'"})}
|
|
113
122
|
</div>
|
|
114
123
|
</div>
|
|
115
124
|
"""
|
|
@@ -117,8 +126,8 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
117
126
|
# Status bars
|
|
118
127
|
status_bars = f"""
|
|
119
128
|
<div class="status-bar">
|
|
120
|
-
<span class="status-pill {
|
|
121
|
-
{
|
|
129
|
+
<span class="status-pill {"success" if jd_connected else "error"}">
|
|
130
|
+
{"✅" if jd_connected else "❌"} JDownloader {"connected" if jd_connected else "disconnected"}
|
|
122
131
|
</span>
|
|
123
132
|
<span class="status-pill {hostname_status_class}">
|
|
124
133
|
{hostname_status_emoji} {hostname_status_text}
|
|
@@ -160,7 +169,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
160
169
|
<div class="input-group">
|
|
161
170
|
<label>URL</label>
|
|
162
171
|
<div class="input-row">
|
|
163
|
-
<input id="urlInput" type="text" readonly value="{shared_state.values[
|
|
172
|
+
<input id="urlInput" type="text" readonly value="{shared_state.values["internal_address"]}" />
|
|
164
173
|
<button id="copyUrl" type="button">Copy</button>
|
|
165
174
|
</div>
|
|
166
175
|
</div>
|
|
@@ -471,12 +480,12 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
471
480
|
</script>
|
|
472
481
|
"""
|
|
473
482
|
# Add logout link for form auth
|
|
474
|
-
logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else
|
|
483
|
+
logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ""
|
|
475
484
|
return render_centered_html(info, footer_content=logout_html)
|
|
476
485
|
|
|
477
|
-
@app.get(
|
|
486
|
+
@app.get("/regenerate-api-key")
|
|
478
487
|
def regenerate_api_key():
|
|
479
488
|
shared_state.generate_api_key()
|
|
480
|
-
return render_success(f
|
|
489
|
+
return render_success(f"API Key replaced!", 5)
|
|
481
490
|
|
|
482
|
-
Server(app, listen=
|
|
491
|
+
Server(app, listen="0.0.0.0", port=shared_state.values["port"]).serve_forever()
|