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.

Files changed (67) hide show
  1. quasarr/__init__.py +157 -67
  2. quasarr/api/__init__.py +126 -43
  3. quasarr/api/arr/__init__.py +197 -78
  4. quasarr/api/captcha/__init__.py +885 -39
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +84 -22
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +236 -487
  9. quasarr/downloads/linkcrypters/al.py +237 -0
  10. quasarr/downloads/linkcrypters/filecrypt.py +178 -31
  11. quasarr/downloads/linkcrypters/hide.py +123 -0
  12. quasarr/downloads/packages/__init__.py +461 -0
  13. quasarr/downloads/sources/al.py +697 -0
  14. quasarr/downloads/sources/by.py +106 -0
  15. quasarr/downloads/sources/dd.py +6 -78
  16. quasarr/downloads/sources/dj.py +7 -0
  17. quasarr/downloads/sources/dt.py +1 -1
  18. quasarr/downloads/sources/dw.py +2 -2
  19. quasarr/downloads/sources/he.py +112 -0
  20. quasarr/downloads/sources/mb.py +47 -0
  21. quasarr/downloads/sources/nk.py +51 -0
  22. quasarr/downloads/sources/nx.py +36 -81
  23. quasarr/downloads/sources/sf.py +27 -4
  24. quasarr/downloads/sources/sj.py +7 -0
  25. quasarr/downloads/sources/sl.py +90 -0
  26. quasarr/downloads/sources/wd.py +110 -0
  27. quasarr/providers/cloudflare.py +204 -0
  28. quasarr/providers/html_images.py +20 -0
  29. quasarr/providers/html_templates.py +48 -39
  30. quasarr/providers/imdb_metadata.py +15 -2
  31. quasarr/providers/myjd_api.py +34 -5
  32. quasarr/providers/notifications.py +30 -5
  33. quasarr/providers/obfuscated.py +35 -0
  34. quasarr/providers/sessions/__init__.py +0 -0
  35. quasarr/providers/sessions/al.py +286 -0
  36. quasarr/providers/sessions/dd.py +78 -0
  37. quasarr/providers/sessions/nx.py +76 -0
  38. quasarr/providers/shared_state.py +347 -20
  39. quasarr/providers/statistics.py +154 -0
  40. quasarr/providers/version.py +1 -1
  41. quasarr/search/__init__.py +112 -36
  42. quasarr/search/sources/al.py +448 -0
  43. quasarr/search/sources/by.py +203 -0
  44. quasarr/search/sources/dd.py +17 -6
  45. quasarr/search/sources/dj.py +213 -0
  46. quasarr/search/sources/dt.py +37 -7
  47. quasarr/search/sources/dw.py +27 -47
  48. quasarr/search/sources/fx.py +27 -29
  49. quasarr/search/sources/he.py +196 -0
  50. quasarr/search/sources/mb.py +195 -0
  51. quasarr/search/sources/nk.py +188 -0
  52. quasarr/search/sources/nx.py +22 -6
  53. quasarr/search/sources/sf.py +143 -151
  54. quasarr/search/sources/sj.py +213 -0
  55. quasarr/search/sources/sl.py +246 -0
  56. quasarr/search/sources/wd.py +208 -0
  57. quasarr/storage/config.py +20 -4
  58. quasarr/storage/setup.py +216 -51
  59. quasarr-1.20.4.dist-info/METADATA +304 -0
  60. quasarr-1.20.4.dist-info/RECORD +72 -0
  61. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
  62. quasarr/providers/tvmaze_metadata.py +0 -23
  63. quasarr-1.4.1.dist-info/METADATA +0 -174
  64. quasarr-1.4.1.dist-info/RECORD +0 -43
  65. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
  66. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
  67. {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, jdownloader_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('- For automated CAPTCHA solutions use SponsorsHelper: "https://github.com/users/rix1337/sponsorship"')
48
- print('- For convenient universal premium downloads use: "https://linksnappy.com/?ref=397097"')
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
- valid_domain = shared_state.extract_valid_hostname(hostname, shorthand)
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.\n")
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
- print(f'Quasarr API now running at: "{shared_state.values['external_address']}"')
209
- print('Use the above URL to set up a "Newznab Indexer" and "SABnzbd Download Client" in Radarr/Sonarr')
210
- print(f'Leave all settings at default and use this API key: "{api_key}" (without quotes)')
211
- print(
212
- 'Optionally set one desired mirror in "API Path" at the advanced indexer settings, e.g. "/api/dropbox/" instead of "/api/"')
213
- print('For more details, check https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions.')
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 jdownloader_connection(shared_state_dict, shared_state_lock):
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
- info(f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"')
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
- try:
255
- shared_state.set_device_settings()
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
- try:
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.start_downloads()
266
- except Exception as e:
267
- print(f"Error starting downloads: {e}")
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 = f"""
34
- <h2>Protected link{plural} found</h2>
35
- <p>{render_button(f"Solve CAPTCHA{plural}", 'primary', {'onclick': "location.href='/captcha'"})}</p>
36
- <a href="https://github.com/users/rix1337/sponsorship" target="_blank">For automated CAPTCHA solutions, use SponsorsHelper!</a>
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="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
60
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
61
+
41
62
  {captcha_hint}
42
- <h2>Setup Instructions</h2>
43
- <p>Use these to set up a <strong>Newznab Indexer</strong> and <strong>SABnzbd Download Client</strong> in Radarr/Sonarr:</p>
44
-
45
- <h3>URL</h3>
46
- <div class="url-wrapper">
47
- <input id="urlInput" class="url-input" type="text" readonly value="{shared_state.values['internal_address']}" />
48
- <button id="copyUrl" class="btn-primary small">Copy</button>
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
- <h3>API Key</h3>
64
- <div class=\"api-key-wrapper\">
65
- <input id=\"apiKeyInput\" class=\"api-key-input\" type=\"password\" readonly value=\"{api_key}\" />
66
- <button id=\"toggleKey\" class=\"btn-secondary small\">Show</button>
67
- <button id=\"copyKey\" class=\"btn-primary small\">Copy</button>
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
- <p>{render_button("Regenerate API key", "secondary", {"onclick": "if(confirm('Regenerate API key?')) location.href='/regenerate-api-key';"})}</p>
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
- toggleBtn.onclick = () => {{
78
- const isHidden = apiInput.type === 'password';
79
- apiInput.type = isHidden ? 'text' : 'password';
80
- toggleBtn.innerText = isHidden ? 'Hide' : 'Show';
81
- }};
82
- copyBtn.onclick = () => {{
83
- apiInput.type = 'text';
84
- apiInput.select();
85
- document.execCommand('copy');
86
- copyBtn.innerText = 'Copied!';
87
- toggleBtn.innerText = 'Hide';
88
- setTimeout(() => {{ copyBtn.innerText = 'Copy'; }}, 2000);
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)