quasarr 1.4.1__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 -67
- quasarr/api/__init__.py +126 -43
- quasarr/api/arr/__init__.py +197 -78
- quasarr/api/captcha/__init__.py +885 -39
- 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 +236 -487
- 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 +48 -39
- quasarr/providers/imdb_metadata.py +15 -2
- quasarr/providers/myjd_api.py +34 -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 +347 -20
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +1 -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 +216 -51
- quasarr-1.20.4.dist-info/METADATA +304 -0
- quasarr-1.20.4.dist-info/RECORD +72 -0
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
- quasarr/providers/tvmaze_metadata.py +0 -23
- quasarr-1.4.1.dist-info/METADATA +0 -174
- quasarr-1.4.1.dist-info/RECORD +0 -43
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.4.1.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,24 +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
|
-
try:
|
|
53
|
-
update_available = version.newer_version_available()
|
|
54
|
-
except Exception as e:
|
|
55
|
-
print(f"Error getting latest version: {str(e)}")
|
|
56
|
-
print('Please manually check: "https://github.com/rix1337/Quasarr/releases/" for more information!')
|
|
57
|
-
update_available = None
|
|
58
|
-
if update_available:
|
|
59
|
-
print("!!! UPDATE AVAILABLE !!!")
|
|
60
|
-
print(f"Please update to the latest version: {update_available} as soon as possible!")
|
|
61
|
-
print('Release notes at: "https://github.com/rix1337/Quasarr/releases/"')
|
|
62
|
-
|
|
63
54
|
port = int('8080')
|
|
64
|
-
|
|
65
55
|
config_path = ""
|
|
66
56
|
if os.environ.get('DOCKER'):
|
|
67
57
|
config_path = "/config"
|
|
@@ -110,12 +100,21 @@ def run():
|
|
|
110
100
|
shared_state.update("database", DataBase)
|
|
111
101
|
supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
|
|
112
102
|
shared_state.update("sites", [key.upper() for key in supported_hostnames])
|
|
113
|
-
shared_state.update("user_agent",
|
|
114
|
-
"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
|
|
115
104
|
shared_state.update("helper_active", False)
|
|
116
105
|
|
|
117
106
|
print(f'Config path: "{config_path}"')
|
|
118
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 =====")
|
|
119
118
|
try:
|
|
120
119
|
if arguments.hostnames:
|
|
121
120
|
hostnames_link = make_raw_pastebin_link(arguments.hostnames)
|
|
@@ -135,7 +134,8 @@ def run():
|
|
|
135
134
|
if results:
|
|
136
135
|
hostnames = Config('Hostnames')
|
|
137
136
|
for shorthand, hostname in results.items():
|
|
138
|
-
|
|
137
|
+
domain_check = shared_state.extract_valid_hostname(hostname, shorthand)
|
|
138
|
+
valid_domain = domain_check.get('domain', None)
|
|
139
139
|
if valid_domain:
|
|
140
140
|
hostnames.save(shorthand, hostname)
|
|
141
141
|
extracted_hostnames += 1
|
|
@@ -153,17 +153,19 @@ def run():
|
|
|
153
153
|
except Exception as e:
|
|
154
154
|
print(f'Error parsing hostnames link: "{e}"')
|
|
155
155
|
|
|
156
|
-
print("\n===== Configuration =====")
|
|
157
|
-
api_key = Config('API').get('key')
|
|
158
|
-
if not api_key:
|
|
159
|
-
api_key = shared_state.generate_api_key()
|
|
160
|
-
|
|
161
156
|
hostnames = get_clean_hostnames(shared_state)
|
|
162
157
|
if not hostnames:
|
|
163
158
|
hostnames_config(shared_state)
|
|
164
159
|
hostnames = get_clean_hostnames(shared_state)
|
|
165
160
|
print(f"You have [{len(hostnames)} of {len(Config._DEFAULT_CONFIG['Hostnames'])}] supported hostnames set up")
|
|
166
|
-
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)
|
|
167
169
|
|
|
168
170
|
dd = Config('Hostnames').get('dd')
|
|
169
171
|
if dd:
|
|
@@ -187,6 +189,7 @@ def run():
|
|
|
187
189
|
if not user or not password or not device:
|
|
188
190
|
jdownloader_config(shared_state)
|
|
189
191
|
|
|
192
|
+
print("\n===== Notifications =====")
|
|
190
193
|
discord_url = ""
|
|
191
194
|
if arguments.discord:
|
|
192
195
|
discord_webhook_pattern = r'^https://discord\.com/api/webhooks/\d+/[\w-]+$'
|
|
@@ -200,17 +203,17 @@ def run():
|
|
|
200
203
|
print("No Discord Webhook URL provided")
|
|
201
204
|
shared_state.update("discord", discord_url)
|
|
202
205
|
|
|
203
|
-
jdownloader = multiprocessing.Process(target=jdownloader_connection,
|
|
204
|
-
args=(shared_state_dict, shared_state_lock))
|
|
205
|
-
jdownloader.start()
|
|
206
|
-
|
|
207
206
|
print("\n===== API Information =====")
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
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"]}"')
|
|
214
217
|
|
|
215
218
|
print("\n===== Quasarr Info Log =====")
|
|
216
219
|
if os.getenv('DEBUG'):
|
|
@@ -222,49 +225,101 @@ def run():
|
|
|
222
225
|
info(f'CAPTCHA-Solution required for {package_count} package{'s' if package_count > 1 else ''} at: '
|
|
223
226
|
f'"{shared_state.values["external_address"]}/captcha"!')
|
|
224
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
|
+
|
|
225
240
|
try:
|
|
226
241
|
get_api(shared_state_dict, shared_state_lock)
|
|
227
242
|
except KeyboardInterrupt:
|
|
243
|
+
jdownloader.kill()
|
|
244
|
+
updater.kill()
|
|
228
245
|
sys.exit(0)
|
|
229
246
|
|
|
230
247
|
|
|
231
|
-
def
|
|
232
|
-
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
233
|
-
|
|
234
|
-
shared_state.set_device_from_config()
|
|
235
|
-
|
|
236
|
-
connection_established = shared_state.get_device() and shared_state.get_device().name
|
|
237
|
-
if not connection_established:
|
|
238
|
-
i = 0
|
|
239
|
-
while i < 10:
|
|
240
|
-
i += 1
|
|
241
|
-
info(f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"')
|
|
242
|
-
time.sleep(60)
|
|
243
|
-
shared_state.set_device_from_config()
|
|
244
|
-
connection_established = shared_state.get_device() and shared_state.get_device().name
|
|
245
|
-
if connection_established:
|
|
246
|
-
break
|
|
247
|
-
|
|
248
|
+
def update_checker(shared_state_dict, shared_state_lock):
|
|
248
249
|
try:
|
|
249
|
-
|
|
250
|
-
except Exception as e:
|
|
251
|
-
info(f'Error connecting to JDownloader: {e}! Stopping Quasarr!')
|
|
252
|
-
sys.exit(1)
|
|
250
|
+
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
253
251
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
except Exception as e:
|
|
257
|
-
print(f"Error checking settings: {e}")
|
|
252
|
+
message = "!!! UPDATE AVAILABLE !!!"
|
|
253
|
+
link = "https://github.com/rix1337/Quasarr/releases/latest"
|
|
258
254
|
|
|
259
|
-
|
|
260
|
-
shared_state.update_jdownloader()
|
|
261
|
-
except Exception as e:
|
|
262
|
-
print(f"Error updating JDownloader: {e}")
|
|
255
|
+
shared_state.update("last_checked_version", f"v.{version.get_version()}")
|
|
263
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)
|
|
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):
|
|
264
283
|
try:
|
|
265
|
-
shared_state.
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
268
323
|
|
|
269
324
|
|
|
270
325
|
class Unbuffered(object):
|
|
@@ -295,6 +350,41 @@ def check_ip():
|
|
|
295
350
|
return ip
|
|
296
351
|
|
|
297
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
|
+
|
|
298
388
|
def make_raw_pastebin_link(url):
|
|
299
389
|
"""
|
|
300
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('/')
|
|
@@ -30,63 +35,141 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
30
35
|
captcha_hint = ""
|
|
31
36
|
if protected:
|
|
32
37
|
plural = 's' if len(protected) > 1 else ''
|
|
33
|
-
captcha_hint
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
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>
|
|
37
57
|
"""
|
|
38
58
|
|
|
39
59
|
info = f"""
|
|
40
|
-
<h1><img src="
|
|
60
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
61
|
+
|
|
41
62
|
{captcha_hint}
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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>
|
|
49
71
|
</div>
|
|
50
|
-
|
|
51
|
-
<script>
|
|
52
|
-
const urlInput = document.getElementById('urlInput');
|
|
53
|
-
const copyUrlBtn = document.getElementById('copyUrl');
|
|
54
|
-
|
|
55
|
-
copyUrlBtn.onclick = () => {{
|
|
56
|
-
urlInput.select();
|
|
57
|
-
document.execCommand('copy');
|
|
58
|
-
copyUrlBtn.innerText = 'Copied!';
|
|
59
|
-
setTimeout(() => {{ copyUrlBtn.innerText = 'Copy'; }}, 2000);
|
|
60
|
-
}};
|
|
61
|
-
</script>
|
|
62
72
|
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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>
|
|
68
107
|
</div>
|
|
69
108
|
|
|
70
|
-
<
|
|
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>
|
|
71
123
|
|
|
72
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
|
+
|
|
73
137
|
const apiInput = document.getElementById('apiKeyInput');
|
|
74
138
|
const toggleBtn = document.getElementById('toggleKey');
|
|
75
139
|
const copyBtn = document.getElementById('copyKey');
|
|
76
140
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
copyBtn.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}}
|
|
90
173
|
</script>
|
|
91
174
|
"""
|
|
92
175
|
return render_centered_html(info)
|