quasarr 1.3.5__py3-none-any.whl → 1.20.4__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 +157 -56
- quasarr/api/__init__.py +141 -36
- quasarr/api/arr/__init__.py +197 -78
- quasarr/api/captcha/__init__.py +897 -42
- quasarr/api/config/__init__.py +23 -0
- quasarr/api/sponsors_helper/__init__.py +84 -22
- quasarr/api/statistics/__init__.py +196 -0
- quasarr/downloads/__init__.py +237 -434
- quasarr/downloads/linkcrypters/al.py +237 -0
- quasarr/downloads/linkcrypters/filecrypt.py +178 -31
- quasarr/downloads/linkcrypters/hide.py +123 -0
- quasarr/downloads/packages/__init__.py +461 -0
- quasarr/downloads/sources/al.py +697 -0
- quasarr/downloads/sources/by.py +106 -0
- quasarr/downloads/sources/dd.py +6 -78
- quasarr/downloads/sources/dj.py +7 -0
- quasarr/downloads/sources/dt.py +1 -1
- quasarr/downloads/sources/dw.py +2 -2
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +51 -0
- quasarr/downloads/sources/nx.py +36 -81
- quasarr/downloads/sources/sf.py +27 -4
- quasarr/downloads/sources/sj.py +7 -0
- quasarr/downloads/sources/sl.py +90 -0
- quasarr/downloads/sources/wd.py +110 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +20 -0
- quasarr/providers/html_templates.py +210 -108
- quasarr/providers/imdb_metadata.py +15 -2
- quasarr/providers/myjd_api.py +36 -5
- quasarr/providers/notifications.py +30 -5
- quasarr/providers/obfuscated.py +35 -0
- quasarr/providers/sessions/__init__.py +0 -0
- quasarr/providers/sessions/al.py +286 -0
- quasarr/providers/sessions/dd.py +78 -0
- quasarr/providers/sessions/nx.py +76 -0
- quasarr/providers/shared_state.py +368 -23
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +60 -1
- quasarr/search/__init__.py +112 -36
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +203 -0
- quasarr/search/sources/dd.py +17 -6
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dt.py +37 -7
- quasarr/search/sources/dw.py +27 -47
- quasarr/search/sources/fx.py +27 -29
- quasarr/search/sources/he.py +196 -0
- quasarr/search/sources/mb.py +195 -0
- quasarr/search/sources/nk.py +188 -0
- quasarr/search/sources/nx.py +22 -6
- quasarr/search/sources/sf.py +143 -151
- quasarr/search/sources/sj.py +213 -0
- quasarr/search/sources/sl.py +246 -0
- quasarr/search/sources/wd.py +208 -0
- quasarr/storage/config.py +20 -4
- quasarr/storage/setup.py +224 -56
- quasarr-1.20.4.dist-info/METADATA +304 -0
- quasarr-1.20.4.dist-info/RECORD +72 -0
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
- quasarr/providers/tvmaze_metadata.py +0 -23
- quasarr-1.3.5.dist-info/METADATA +0 -174
- quasarr-1.3.5.dist-info/RECORD +0 -43
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
quasarr/__init__.py
CHANGED
|
@@ -17,8 +17,10 @@ import requests
|
|
|
17
17
|
from quasarr.api import get_api
|
|
18
18
|
from quasarr.providers import shared_state, version
|
|
19
19
|
from quasarr.providers.log import info, debug
|
|
20
|
+
from quasarr.providers.notifications import send_discord_message
|
|
20
21
|
from quasarr.storage.config import Config, get_clean_hostnames
|
|
21
|
-
from quasarr.storage.setup import path_config, hostnames_config, hostname_credentials_config,
|
|
22
|
+
from quasarr.storage.setup import path_config, hostnames_config, hostname_credentials_config, flaresolverr_config, \
|
|
23
|
+
jdownloader_config
|
|
22
24
|
from quasarr.storage.sqlite_database import DataBase
|
|
23
25
|
|
|
24
26
|
|
|
@@ -44,13 +46,12 @@ def run():
|
|
|
44
46
|
└────────────────────────────────────┘""")
|
|
45
47
|
|
|
46
48
|
print("\n===== Recommended Services =====")
|
|
47
|
-
print('
|
|
48
|
-
print(
|
|
49
|
+
print('For convenient universal premium downloads use: "https://linksnappy.com/?ref=397097"')
|
|
50
|
+
print(
|
|
51
|
+
'Sponsors get automated CAPTCHA solutions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper"')
|
|
49
52
|
|
|
50
53
|
print("\n===== Startup Info =====")
|
|
51
|
-
|
|
52
54
|
port = int('8080')
|
|
53
|
-
|
|
54
55
|
config_path = ""
|
|
55
56
|
if os.environ.get('DOCKER'):
|
|
56
57
|
config_path = "/config"
|
|
@@ -99,12 +100,21 @@ def run():
|
|
|
99
100
|
shared_state.update("database", DataBase)
|
|
100
101
|
supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
|
|
101
102
|
shared_state.update("sites", [key.upper() for key in supported_hostnames])
|
|
102
|
-
shared_state.update("user_agent",
|
|
103
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
|
|
103
|
+
shared_state.update("user_agent", "") # will be set by FlareSolverr
|
|
104
104
|
shared_state.update("helper_active", False)
|
|
105
105
|
|
|
106
106
|
print(f'Config path: "{config_path}"')
|
|
107
107
|
|
|
108
|
+
flaresolverr_url = Config('FlareSolverr').get('url')
|
|
109
|
+
if not flaresolverr_url:
|
|
110
|
+
flaresolverr_config(shared_state)
|
|
111
|
+
else:
|
|
112
|
+
print(f'Flaresolverr URL: "{flaresolverr_url}"')
|
|
113
|
+
flaresolverr_check = check_flaresolverr(shared_state, flaresolverr_url)
|
|
114
|
+
if flaresolverr_check:
|
|
115
|
+
print(f'User Agent: "{shared_state.values["user_agent"]}"')
|
|
116
|
+
|
|
117
|
+
print("\n===== Hostnames =====")
|
|
108
118
|
try:
|
|
109
119
|
if arguments.hostnames:
|
|
110
120
|
hostnames_link = make_raw_pastebin_link(arguments.hostnames)
|
|
@@ -124,7 +134,8 @@ def run():
|
|
|
124
134
|
if results:
|
|
125
135
|
hostnames = Config('Hostnames')
|
|
126
136
|
for shorthand, hostname in results.items():
|
|
127
|
-
|
|
137
|
+
domain_check = shared_state.extract_valid_hostname(hostname, shorthand)
|
|
138
|
+
valid_domain = domain_check.get('domain', None)
|
|
128
139
|
if valid_domain:
|
|
129
140
|
hostnames.save(shorthand, hostname)
|
|
130
141
|
extracted_hostnames += 1
|
|
@@ -142,17 +153,19 @@ def run():
|
|
|
142
153
|
except Exception as e:
|
|
143
154
|
print(f'Error parsing hostnames link: "{e}"')
|
|
144
155
|
|
|
145
|
-
print("\n===== Configuration =====")
|
|
146
|
-
api_key = Config('API').get('key')
|
|
147
|
-
if not api_key:
|
|
148
|
-
api_key = shared_state.generate_api_key()
|
|
149
|
-
|
|
150
156
|
hostnames = get_clean_hostnames(shared_state)
|
|
151
157
|
if not hostnames:
|
|
152
158
|
hostnames_config(shared_state)
|
|
153
159
|
hostnames = get_clean_hostnames(shared_state)
|
|
154
160
|
print(f"You have [{len(hostnames)} of {len(Config._DEFAULT_CONFIG['Hostnames'])}] supported hostnames set up")
|
|
155
|
-
print(f"For efficiency it is recommended to set up as few hostnames as needed
|
|
161
|
+
print(f"For efficiency it is recommended to set up as few hostnames as needed.")
|
|
162
|
+
|
|
163
|
+
al = Config('Hostnames').get('al')
|
|
164
|
+
if al:
|
|
165
|
+
user = Config('AL').get('user')
|
|
166
|
+
password = Config('AL').get('password')
|
|
167
|
+
if not user or not password:
|
|
168
|
+
hostname_credentials_config(shared_state, "AL", al)
|
|
156
169
|
|
|
157
170
|
dd = Config('Hostnames').get('dd')
|
|
158
171
|
if dd:
|
|
@@ -176,6 +189,7 @@ def run():
|
|
|
176
189
|
if not user or not password or not device:
|
|
177
190
|
jdownloader_config(shared_state)
|
|
178
191
|
|
|
192
|
+
print("\n===== Notifications =====")
|
|
179
193
|
discord_url = ""
|
|
180
194
|
if arguments.discord:
|
|
181
195
|
discord_webhook_pattern = r'^https://discord\.com/api/webhooks/\d+/[\w-]+$'
|
|
@@ -189,17 +203,17 @@ def run():
|
|
|
189
203
|
print("No Discord Webhook URL provided")
|
|
190
204
|
shared_state.update("discord", discord_url)
|
|
191
205
|
|
|
192
|
-
jdownloader = multiprocessing.Process(target=jdownloader_connection,
|
|
193
|
-
args=(shared_state_dict, shared_state_lock))
|
|
194
|
-
jdownloader.start()
|
|
195
|
-
|
|
196
206
|
print("\n===== API Information =====")
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
print('
|
|
207
|
+
api_key = Config('API').get('key')
|
|
208
|
+
if not api_key:
|
|
209
|
+
api_key = shared_state.generate_api_key()
|
|
210
|
+
|
|
211
|
+
print('Setup instructions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions"')
|
|
212
|
+
print(f'URL: "{shared_state.values['internal_address']}"')
|
|
213
|
+
print(f'API key: "{api_key}" (without quotes)')
|
|
214
|
+
|
|
215
|
+
if external_address != internal_address:
|
|
216
|
+
print(f'External URL: "{shared_state.values["external_address"]}"')
|
|
203
217
|
|
|
204
218
|
print("\n===== Quasarr Info Log =====")
|
|
205
219
|
if os.getenv('DEBUG'):
|
|
@@ -211,49 +225,101 @@ def run():
|
|
|
211
225
|
info(f'CAPTCHA-Solution required for {package_count} package{'s' if package_count > 1 else ''} at: '
|
|
212
226
|
f'"{shared_state.values["external_address"]}/captcha"!')
|
|
213
227
|
|
|
228
|
+
jdownloader = multiprocessing.Process(
|
|
229
|
+
target=jdownloader_connection,
|
|
230
|
+
args=(shared_state_dict, shared_state_lock)
|
|
231
|
+
)
|
|
232
|
+
jdownloader.start()
|
|
233
|
+
|
|
234
|
+
updater = multiprocessing.Process(
|
|
235
|
+
target=update_checker,
|
|
236
|
+
args=(shared_state_dict, shared_state_lock)
|
|
237
|
+
)
|
|
238
|
+
updater.start()
|
|
239
|
+
|
|
214
240
|
try:
|
|
215
241
|
get_api(shared_state_dict, shared_state_lock)
|
|
216
242
|
except KeyboardInterrupt:
|
|
243
|
+
jdownloader.kill()
|
|
244
|
+
updater.kill()
|
|
217
245
|
sys.exit(0)
|
|
218
246
|
|
|
219
247
|
|
|
220
|
-
def
|
|
221
|
-
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
222
|
-
|
|
223
|
-
shared_state.set_device_from_config()
|
|
224
|
-
|
|
225
|
-
connection_established = shared_state.get_device() and shared_state.get_device().name
|
|
226
|
-
if not connection_established:
|
|
227
|
-
i = 0
|
|
228
|
-
while i < 10:
|
|
229
|
-
i += 1
|
|
230
|
-
info(f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"')
|
|
231
|
-
time.sleep(60)
|
|
232
|
-
shared_state.set_device_from_config()
|
|
233
|
-
connection_established = shared_state.get_device() and shared_state.get_device().name
|
|
234
|
-
if connection_established:
|
|
235
|
-
break
|
|
236
|
-
|
|
248
|
+
def update_checker(shared_state_dict, shared_state_lock):
|
|
237
249
|
try:
|
|
238
|
-
|
|
239
|
-
except Exception as e:
|
|
240
|
-
info(f'Error connecting to JDownloader: {e}! Stopping Quasarr!')
|
|
241
|
-
sys.exit(1)
|
|
250
|
+
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
242
251
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
except Exception as e:
|
|
246
|
-
print(f"Error checking settings: {e}")
|
|
252
|
+
message = "!!! UPDATE AVAILABLE !!!"
|
|
253
|
+
link = "https://github.com/rix1337/Quasarr/releases/latest"
|
|
247
254
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
255
|
+
shared_state.update("last_checked_version", f"v.{version.get_version()}")
|
|
256
|
+
|
|
257
|
+
while True:
|
|
258
|
+
try:
|
|
259
|
+
update_available = version.newer_version_available()
|
|
260
|
+
except Exception as e:
|
|
261
|
+
info(f"Error getting latest version: {e}")
|
|
262
|
+
info(f'Please manually check: "{link}" for more information!')
|
|
263
|
+
update_available = None
|
|
264
|
+
|
|
265
|
+
if update_available and shared_state.values["last_checked_version"] != update_available:
|
|
266
|
+
shared_state.update("last_checked_version", update_available)
|
|
267
|
+
info(message)
|
|
268
|
+
info(f"Please update to {update_available} as soon as possible!")
|
|
269
|
+
info(f'Release notes at: "{link}"')
|
|
270
|
+
update_available = {
|
|
271
|
+
"version": update_available,
|
|
272
|
+
"link": link
|
|
273
|
+
}
|
|
274
|
+
send_discord_message(shared_state, message, "quasarr_update", details=update_available)
|
|
252
275
|
|
|
276
|
+
# wait one hour before next check
|
|
277
|
+
time.sleep(60 * 60)
|
|
278
|
+
except KeyboardInterrupt:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def jdownloader_connection(shared_state_dict, shared_state_lock):
|
|
253
283
|
try:
|
|
254
|
-
shared_state.
|
|
255
|
-
|
|
256
|
-
|
|
284
|
+
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
285
|
+
|
|
286
|
+
shared_state.set_device_from_config()
|
|
287
|
+
|
|
288
|
+
connection_established = shared_state.get_device() and shared_state.get_device().name
|
|
289
|
+
if not connection_established:
|
|
290
|
+
i = 0
|
|
291
|
+
while i < 10:
|
|
292
|
+
i += 1
|
|
293
|
+
info(f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"')
|
|
294
|
+
time.sleep(60)
|
|
295
|
+
shared_state.set_device_from_config()
|
|
296
|
+
connection_established = shared_state.get_device() and shared_state.get_device().name
|
|
297
|
+
if connection_established:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
info(f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"')
|
|
302
|
+
except Exception as e:
|
|
303
|
+
info(f'Error connecting to JDownloader: {e}! Stopping Quasarr!')
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
shared_state.set_device_settings()
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print(f"Error checking settings: {e}")
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
shared_state.update_jdownloader()
|
|
313
|
+
except Exception as e:
|
|
314
|
+
print(f"Error updating JDownloader: {e}")
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
shared_state.start_downloads()
|
|
318
|
+
except Exception as e:
|
|
319
|
+
print(f"Error starting downloads: {e}")
|
|
320
|
+
|
|
321
|
+
except KeyboardInterrupt:
|
|
322
|
+
pass
|
|
257
323
|
|
|
258
324
|
|
|
259
325
|
class Unbuffered(object):
|
|
@@ -284,6 +350,41 @@ def check_ip():
|
|
|
284
350
|
return ip
|
|
285
351
|
|
|
286
352
|
|
|
353
|
+
def check_flaresolverr(shared_state, flaresolverr_url):
|
|
354
|
+
# Ensure it ends with /v<digit+>
|
|
355
|
+
if not re.search(r"/v\d+$", flaresolverr_url):
|
|
356
|
+
print(f"FlareSolverr URL does not end with /v#: {flaresolverr_url}")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
# Try sending a simple test request
|
|
360
|
+
headers = {"Content-Type": "application/json"}
|
|
361
|
+
data = {
|
|
362
|
+
"cmd": "request.get",
|
|
363
|
+
"url": "http://www.google.com/",
|
|
364
|
+
"maxTimeout": 10000
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
response = requests.post(flaresolverr_url, headers=headers, json=data, timeout=10)
|
|
369
|
+
response.raise_for_status()
|
|
370
|
+
json_data = response.json()
|
|
371
|
+
|
|
372
|
+
# Check if the structure looks like a valid FlareSolverr response
|
|
373
|
+
if "status" in json_data and json_data["status"] == "ok":
|
|
374
|
+
solution = json_data["solution"]
|
|
375
|
+
solution_ua = solution.get("userAgent", None)
|
|
376
|
+
if solution_ua:
|
|
377
|
+
shared_state.update("user_agent", solution_ua)
|
|
378
|
+
return True
|
|
379
|
+
else:
|
|
380
|
+
print(f"Unexpected FlareSolverr response: {json_data}")
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
print(f"Failed to connect to FlareSolverr: {e}")
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
|
|
287
388
|
def make_raw_pastebin_link(url):
|
|
288
389
|
"""
|
|
289
390
|
Takes a Pastebin URL and ensures it is a raw link.
|
quasarr/api/__init__.py
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
from bottle import Bottle
|
|
6
6
|
|
|
7
|
+
import quasarr.providers.html_images as images
|
|
7
8
|
from quasarr.api.arr import setup_arr_routes
|
|
8
9
|
from quasarr.api.captcha import setup_captcha_routes
|
|
10
|
+
from quasarr.api.config import setup_config
|
|
9
11
|
from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
12
|
+
from quasarr.api.statistics import setup_statistics
|
|
10
13
|
from quasarr.providers import shared_state
|
|
11
14
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
12
15
|
from quasarr.providers.web_server import Server
|
|
@@ -20,6 +23,8 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
20
23
|
|
|
21
24
|
setup_arr_routes(app)
|
|
22
25
|
setup_captcha_routes(app)
|
|
26
|
+
setup_config(app, shared_state)
|
|
27
|
+
setup_statistics(app, shared_state)
|
|
23
28
|
setup_sponsors_helper_routes(app)
|
|
24
29
|
|
|
25
30
|
@app.get('/')
|
|
@@ -29,43 +34,143 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
29
34
|
|
|
30
35
|
captcha_hint = ""
|
|
31
36
|
if protected:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
plural = 's' if len(protected) > 1 else ''
|
|
38
|
+
captcha_hint += f"""
|
|
39
|
+
<div class="section">
|
|
40
|
+
<h2>🔒 Link{plural} waiting for CAPTCHA solution</h2>
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if not shared_state.values.get("helper_active"):
|
|
44
|
+
captcha_hint += f"""
|
|
45
|
+
<p>
|
|
46
|
+
<a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" target="_blank">
|
|
47
|
+
Sponsors get automated CAPTCHA solutions!
|
|
48
|
+
</a>
|
|
49
|
+
</p>
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
plural = 's' if len(protected) > 1 else ''
|
|
53
|
+
captcha_hint += f"""
|
|
54
|
+
<p>{render_button(f"Solve CAPTCHA{plural}", 'primary', {'onclick': "location.href='/captcha'"})}</p>
|
|
55
|
+
</div>
|
|
56
|
+
<hr>
|
|
57
|
+
"""
|
|
47
58
|
|
|
48
59
|
info = f"""
|
|
49
|
-
<h1
|
|
60
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
61
|
+
|
|
50
62
|
{captcha_hint}
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
|
|
64
|
+
<div class="section">
|
|
65
|
+
<h2>📖 Setup Instructions</h2>
|
|
66
|
+
<p>
|
|
67
|
+
<a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank">
|
|
68
|
+
Refer to the README for detailed instructions.
|
|
69
|
+
</a>
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<hr>
|
|
74
|
+
|
|
75
|
+
<div class="section">
|
|
76
|
+
<h2>⚙️ API Configuration</h2>
|
|
77
|
+
<p>Use the URL and API Key below to set up a <strong>Newznab Indexer</strong> and <strong>SABnzbd Download Client</strong> in Radarr/Sonarr:</p>
|
|
78
|
+
|
|
79
|
+
<details id="apiDetails">
|
|
80
|
+
<summary id="apiSummary">Show API Settings</summary>
|
|
81
|
+
<div class="api-settings">
|
|
82
|
+
|
|
83
|
+
<h3>URL</h3>
|
|
84
|
+
<div class="url-wrapper">
|
|
85
|
+
<input id="urlInput" class="copy-input" type="text" readonly value="{shared_state.values['internal_address']}" />
|
|
86
|
+
<button id="copyUrl" class="btn-primary small">Copy</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<h3>API Key</h3>
|
|
90
|
+
<div class="api-key-wrapper">
|
|
91
|
+
<input id="apiKeyInput" class="copy-input" type="password" readonly value="{api_key}" />
|
|
92
|
+
<button id="toggleKey" class="btn-secondary small">Show</button>
|
|
93
|
+
<button id="copyKey" class="btn-primary small">Copy</button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<p>{render_button("Regenerate API key", "secondary", {"onclick": "if(confirm('Regenerate API key?')) location.href='/regenerate-api-key';"})}</p>
|
|
97
|
+
</div>
|
|
98
|
+
</details>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<hr>
|
|
102
|
+
|
|
103
|
+
<div class="section">
|
|
104
|
+
<h2>🔧 Quick Actions</h2>
|
|
105
|
+
<p><button class="btn-primary" onclick="location.href='/hostnames'">Update Hostnames</button></p>
|
|
106
|
+
<p><button class="btn-primary" onclick="location.href='/statistics'">View Statistics</button></p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
.section {{ margin: 20px 0; }}
|
|
111
|
+
.api-settings {{ padding: 15px 0; }}
|
|
112
|
+
hr {{ margin: 25px 0; border: none; border-top: 1px solid #ddd; }}
|
|
113
|
+
details {{ margin: 10px 0; }}
|
|
114
|
+
summary {{
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
padding: 8px 0;
|
|
117
|
+
font-weight: 500;
|
|
118
|
+
}}
|
|
119
|
+
summary:hover {{
|
|
120
|
+
color: #0066cc;
|
|
121
|
+
}}
|
|
122
|
+
</style>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
const urlInput = document.getElementById('urlInput');
|
|
126
|
+
const copyUrlBtn = document.getElementById('copyUrl');
|
|
127
|
+
|
|
128
|
+
if (copyUrlBtn) {{
|
|
129
|
+
copyUrlBtn.onclick = () => {{
|
|
130
|
+
urlInput.select();
|
|
131
|
+
document.execCommand('copy');
|
|
132
|
+
copyUrlBtn.innerText = 'Copied!';
|
|
133
|
+
setTimeout(() => {{ copyUrlBtn.innerText = 'Copy'; }}, 2000);
|
|
134
|
+
}};
|
|
135
|
+
}}
|
|
136
|
+
|
|
137
|
+
const apiInput = document.getElementById('apiKeyInput');
|
|
138
|
+
const toggleBtn = document.getElementById('toggleKey');
|
|
139
|
+
const copyBtn = document.getElementById('copyKey');
|
|
140
|
+
|
|
141
|
+
if (toggleBtn) {{
|
|
142
|
+
toggleBtn.onclick = () => {{
|
|
143
|
+
const isHidden = apiInput.type === 'password';
|
|
144
|
+
apiInput.type = isHidden ? 'text' : 'password';
|
|
145
|
+
toggleBtn.innerText = isHidden ? 'Hide' : 'Show';
|
|
146
|
+
}};
|
|
147
|
+
}}
|
|
148
|
+
|
|
149
|
+
if (copyBtn) {{
|
|
150
|
+
copyBtn.onclick = () => {{
|
|
151
|
+
apiInput.type = 'text';
|
|
152
|
+
apiInput.select();
|
|
153
|
+
document.execCommand('copy');
|
|
154
|
+
copyBtn.innerText = 'Copied!';
|
|
155
|
+
toggleBtn.innerText = 'Hide';
|
|
156
|
+
setTimeout(() => {{ copyBtn.innerText = 'Copy'; }}, 2000);
|
|
157
|
+
}};
|
|
158
|
+
}}
|
|
159
|
+
|
|
160
|
+
// Handle details toggle
|
|
161
|
+
const apiDetails = document.getElementById('apiDetails');
|
|
162
|
+
const apiSummary = document.getElementById('apiSummary');
|
|
163
|
+
|
|
164
|
+
if (apiDetails && apiSummary) {{
|
|
165
|
+
apiDetails.addEventListener('toggle', () => {{
|
|
166
|
+
if (apiDetails.open) {{
|
|
167
|
+
apiSummary.textContent = 'Hide API Settings';
|
|
168
|
+
}} else {{
|
|
169
|
+
apiSummary.textContent = 'Show API Settings';
|
|
170
|
+
}}
|
|
171
|
+
}});
|
|
172
|
+
}}
|
|
173
|
+
</script>
|
|
69
174
|
"""
|
|
70
175
|
return render_centered_html(info)
|
|
71
176
|
|
|
@@ -74,8 +179,8 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
74
179
|
api_key = shared_state.generate_api_key()
|
|
75
180
|
return f"""
|
|
76
181
|
<script>
|
|
77
|
-
|
|
78
|
-
|
|
182
|
+
alert('API key replaced with: {api_key}');
|
|
183
|
+
window.location.href = '/';
|
|
79
184
|
</script>
|
|
80
185
|
"""
|
|
81
186
|
|