quasarr 2.0.0__py3-none-any.whl → 2.1.1__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/api/__init__.py CHANGED
@@ -25,7 +25,7 @@ def get_api(shared_state_dict, shared_state_lock):
25
25
 
26
26
  # Auth: routes must come first, then hook
27
27
  add_auth_routes(app)
28
- add_auth_hook(app, whitelist_prefixes=['/api', '/api/' '/sponsors_helper/', '/download/'])
28
+ add_auth_hook(app, whitelist_prefixes=['/api', '/api/', '/sponsors_helper/', '/download/'])
29
29
 
30
30
  setup_arr_routes(app)
31
31
  setup_captcha_routes(app)
@@ -66,7 +66,7 @@ def get_api(shared_state_dict, shared_state_lock):
66
66
  # JDownloader status
67
67
  jd_status = f"""
68
68
  <div class="status-bar">
69
- <span class="status-item {'status-ok' if jd_connected else 'status-error'}">
69
+ <span class="status-pill {'success' if jd_connected else 'error'}">
70
70
  {'✅' if jd_connected else '❌'} JDownloader {'Connected' if jd_connected else 'Disconnected'}
71
71
  </span>
72
72
  </div>
@@ -389,8 +389,8 @@ def get_api(shared_state_dict, shared_state_lock):
389
389
  </script>
390
390
  """
391
391
  # Add logout link for form auth
392
- logout_html = '<div class="logout-link"><a href="/logout">Logout</a></div>' if show_logout_link() else ''
393
- return render_centered_html(info + logout_html)
392
+ logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ''
393
+ return render_centered_html(info, footer_content=logout_html)
394
394
 
395
395
  @app.get('/regenerate-api-key')
396
396
  def regenerate_api_key():
@@ -6,7 +6,6 @@ import traceback
6
6
  import xml.sax.saxutils as sax_utils
7
7
  from base64 import urlsafe_b64decode
8
8
  from datetime import datetime
9
- from functools import wraps
10
9
  from urllib.parse import urlparse, parse_qs
11
10
  from xml.etree import ElementTree
12
11
 
@@ -15,23 +14,45 @@ from bottle import abort, request
15
14
  from quasarr.downloads import download
16
15
  from quasarr.downloads.packages import get_packages, delete_package
17
16
  from quasarr.providers import shared_state
17
+ from quasarr.providers.auth import require_api_key
18
18
  from quasarr.providers.log import info, debug
19
19
  from quasarr.providers.version import get_version
20
20
  from quasarr.search import get_search_results
21
- from quasarr.storage.config import Config
22
21
 
23
22
 
24
- def require_api_key(func):
25
- @wraps(func)
26
- def decorated(*args, **kwargs):
27
- api_key = Config('API').get('key')
28
- if not request.query.apikey:
29
- return abort(401, "Missing API key")
30
- if request.query.apikey != api_key:
31
- return abort(403, "Invalid API key")
32
- return func(*args, **kwargs)
33
-
34
- return decorated
23
+ def parse_payload(payload_str):
24
+ """
25
+ Parse the base64-encoded payload string into its components.
26
+
27
+ Supports both legacy 6-field format and new 7-field format:
28
+ - Legacy (6 fields): title|url|mirror|size_mb|password|imdb_id
29
+ - New (7 fields): title|url|mirror|size_mb|password|imdb_id|source_key
30
+
31
+ Returns:
32
+ dict with keys: title, url, mirror, size_mb, password, imdb_id, source_key
33
+ """
34
+ decoded = urlsafe_b64decode(payload_str.encode()).decode()
35
+ parts = decoded.split("|")
36
+
37
+ if len(parts) == 6:
38
+ # Legacy format - no source_key provided
39
+ title, url, mirror, size_mb, password, imdb_id = parts
40
+ source_key = None
41
+ elif len(parts) == 7:
42
+ # New format with source_key
43
+ title, url, mirror, size_mb, password, imdb_id, source_key = parts
44
+ else:
45
+ raise ValueError(f"expected 6 or 7 fields, got {len(parts)}")
46
+
47
+ return {
48
+ "title": title,
49
+ "url": url,
50
+ "mirror": None if mirror == "None" else mirror,
51
+ "size_mb": size_mb,
52
+ "password": password if password else None,
53
+ "imdb_id": imdb_id if imdb_id else None,
54
+ "source_key": source_key if source_key else None
55
+ }
35
56
 
36
57
 
37
58
  def setup_arr_routes(app):
@@ -39,13 +60,17 @@ def setup_arr_routes(app):
39
60
  def fake_nzb_file():
40
61
  payload = request.query.payload
41
62
  decoded_payload = urlsafe_b64decode(payload).decode("utf-8").split("|")
63
+
64
+ # Support both 6 and 7 field formats
42
65
  title = decoded_payload[0]
43
66
  url = decoded_payload[1]
44
67
  mirror = decoded_payload[2]
45
68
  size_mb = decoded_payload[3]
46
69
  password = decoded_payload[4]
47
70
  imdb_id = decoded_payload[5]
48
- return f'<nzb><file title="{title}" url="{url}" mirror="{mirror}" size_mb="{size_mb}" password="{password}" imdb_id="{imdb_id}"/></nzb>'
71
+ source_key = decoded_payload[6] if len(decoded_payload) > 6 else ""
72
+
73
+ return f'<nzb><file title="{title}" url="{url}" mirror="{mirror}" size_mb="{size_mb}" password="{password}" imdb_id="{imdb_id}" source_key="{source_key}"/></nzb>'
49
74
 
50
75
  @app.post('/api')
51
76
  @require_api_key
@@ -65,10 +90,12 @@ def setup_arr_routes(app):
65
90
  size_mb = root.find(".//file").attrib["size_mb"]
66
91
  password = root.find(".//file").attrib.get("password")
67
92
  imdb_id = root.find(".//file").attrib.get("imdb_id")
93
+ source_key = root.find(".//file").attrib.get("source_key") or None
68
94
 
69
95
  info(f'Attempting download for "{title}"')
70
96
  request_from = request.headers.get('User-Agent')
71
- downloaded = download(shared_state, request_from, title, url, mirror, size_mb, password, imdb_id)
97
+ downloaded = download(shared_state, request_from, title, url, mirror, size_mb, password, imdb_id,
98
+ source_key)
72
99
  try:
73
100
  success = downloaded["success"]
74
101
  package_id = downloaded["package_id"]
@@ -166,37 +193,31 @@ def setup_arr_routes(app):
166
193
  if not payload:
167
194
  abort(400, "missing 'payload' parameter in URL")
168
195
 
169
- title = url = mirror = size_mb = password = imdb_id = None
170
196
  try:
171
- decoded = urlsafe_b64decode(payload.encode()).decode()
172
- parts = decoded.split("|")
173
- if len(parts) != 6:
174
- raise ValueError(f"expected 6 fields, got {len(parts)}")
175
- title, url, mirror, size_mb, password, imdb_id = parts
197
+ parsed_payload = parse_payload(payload)
176
198
  except Exception as e:
177
199
  abort(400, f"invalid payload format: {e}")
178
200
 
179
- mirror = None if mirror == "None" else mirror
180
-
181
201
  nzo_ids = []
182
- info(f'Attempting download for "{title}"')
202
+ info(f'Attempting download for "{parsed_payload["title"]}"')
183
203
  request_from = "lazylibrarian"
184
204
 
185
205
  downloaded = download(
186
206
  shared_state,
187
207
  request_from,
188
- title,
189
- url,
190
- mirror,
191
- size_mb,
192
- password or None,
193
- imdb_id or None,
208
+ parsed_payload["title"],
209
+ parsed_payload["url"],
210
+ parsed_payload["mirror"],
211
+ parsed_payload["size_mb"],
212
+ parsed_payload["password"],
213
+ parsed_payload["imdb_id"],
214
+ parsed_payload["source_key"],
194
215
  )
195
216
 
196
217
  try:
197
218
  success = downloaded["success"]
198
219
  package_id = downloaded["package_id"]
199
- title = downloaded.get("title", title)
220
+ title = downloaded.get("title", parsed_payload["title"])
200
221
 
201
222
  if success:
202
223
  info(f'"{title}" added successfully!')
@@ -204,7 +225,7 @@ def setup_arr_routes(app):
204
225
  info(f'"{title}" added unsuccessfully! See log for details.')
205
226
  nzo_ids.append(package_id)
206
227
  except KeyError:
207
- info(f'Failed to download "{title}" - no package_id returned')
228
+ info(f'Failed to download "{parsed_payload["title"]}" - no package_id returned')
208
229
 
209
230
  return {
210
231
  "status": True,
@@ -353,7 +374,8 @@ def setup_arr_routes(app):
353
374
  <enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
354
375
  </item>'''
355
376
 
356
- requires_placeholder_item = not getattr(request.query, 'imdbid', '') and not getattr(request.query, 'q', '')
377
+ requires_placeholder_item = not getattr(request.query, 'imdbid', '') and not getattr(request.query,
378
+ 'q', '')
357
379
  if requires_placeholder_item and not items:
358
380
  items = f'''
359
381
  <item>
@@ -235,7 +235,7 @@ def setup_config(app, shared_state):
235
235
  }}
236
236
  </script>
237
237
  '''
238
- return render_form("Configure FlareSolverr", form_html)
238
+ return render_form("FlareSolverr", form_html)
239
239
 
240
240
  @app.post('/api/flaresolverr')
241
241
  def set_flaresolverr_url():
@@ -13,21 +13,17 @@ def setup_packages_routes(app):
13
13
  def delete_package_route(package_id):
14
14
  success = delete_package(shared_state, package_id)
15
15
 
16
+ # Redirect back to packages page with status message via query param
17
+ from bottle import redirect
16
18
  if success:
17
- return render_centered_html(f'''
18
- <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
19
- <p>✅ Package deleted successfully.</p>
20
- <p>{render_button("Back to Packages", "primary", {"onclick": "location.href='/packages'"})}</p>
21
- ''')
19
+ redirect('/packages?deleted=1')
22
20
  else:
23
- return render_centered_html(f'''
24
- <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
25
- <p>❌ Failed to delete package.</p>
26
- <p>{render_button("Back to Packages", "secondary", {"onclick": "location.href='/packages'"})}</p>
27
- ''')
21
+ redirect('/packages?deleted=0')
28
22
 
29
23
  @app.get('/packages')
30
24
  def packages_status():
25
+ from bottle import request
26
+
31
27
  try:
32
28
  device = shared_state.values["device"]
33
29
  except KeyError:
@@ -41,6 +37,14 @@ def setup_packages_routes(app):
41
37
  <p>{back_btn}</p>
42
38
  ''')
43
39
 
40
+ # Check for delete status from redirect
41
+ deleted = request.query.get('deleted')
42
+ status_message = ""
43
+ if deleted == '1':
44
+ status_message = '<div class="status-message success">✅ Package deleted successfully.</div>'
45
+ elif deleted == '0':
46
+ status_message = '<div class="status-message error">❌ Failed to delete package.</div>'
47
+
44
48
  # Get packages data
45
49
  downloads = get_packages(shared_state)
46
50
  queue = downloads.get('queue', [])
@@ -143,29 +147,43 @@ def setup_packages_routes(app):
143
147
  def render_history_item(item):
144
148
  name = item.get('name', 'Unknown')
145
149
  status = item.get('status', 'Unknown')
146
- category = item.get('category', 'not_quasarr')
147
150
  bytes_val = item.get('bytes', 0)
148
- is_archive = item.get('is_archive')
149
- extraction_ok = item.get('extraction_ok', False)
151
+ category = item.get('category', 'not_quasarr')
152
+ is_archive = item.get('is_archive', False)
153
+ extraction_status = item.get('extraction_status', '')
150
154
  fail_message = item.get('fail_message', '')
151
155
  nzo_id = item.get('nzo_id', '')
152
156
 
153
- status_emoji = '✅' if status == 'Completed' else ''
154
-
155
- archive_badge = ''
156
- if is_archive:
157
- archive_badge = '<span class="badge extracted">📁 EXTRACTED</span>' if extraction_ok else '<span class="badge pending">📁 NOT EXTRACTED</span>'
157
+ is_error = status.lower() in ['failed', 'error'] or fail_message
158
+ card_class = 'package-card error' if is_error else 'package-card'
158
159
 
159
160
  cat_emoji = get_category_emoji(category)
160
161
  size_str = format_size(bytes_val=bytes_val)
161
162
 
163
+ archive_badge = ''
164
+ if is_archive:
165
+ if extraction_status == 'SUCCESSFUL':
166
+ archive_badge = '<span class="badge extracted">✅ EXTRACTED</span>'
167
+ elif extraction_status == 'RUNNING':
168
+ archive_badge = '<span class="badge pending">⏳ EXTRACTING</span>'
169
+ else:
170
+ archive_badge = '<span class="badge archive">📁 ARCHIVE</span>'
171
+
172
+ status_emoji = '❌' if is_error else '✅'
162
173
  error_html = f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ''
163
- error_class = 'error' if status != 'Completed' else ''
164
174
 
165
- delete_btn = f'<div class="package-actions right-only"><button class="btn-small danger" onclick="confirmDelete(\'{nzo_id}\', \'{escape_js(name)}\')">🗑️</button></div>' if nzo_id else ''
175
+ # Delete button for history items
176
+ if nzo_id:
177
+ actions = f'''
178
+ <div class="package-actions right-only">
179
+ <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{escape_js(name)}')">🗑️</button>
180
+ </div>
181
+ '''
182
+ else:
183
+ actions = ''
166
184
 
167
185
  return f'''
168
- <div class="package-card {error_class}">
186
+ <div class="{card_class}">
169
187
  <div class="package-header">
170
188
  <span class="status-emoji">{status_emoji}</span>
171
189
  <span class="package-name">{name}</span>
@@ -176,24 +194,46 @@ def setup_packages_routes(app):
176
194
  <span>{cat_emoji} {category}</span>
177
195
  </div>
178
196
  {error_html}
179
- {delete_btn}
197
+ {actions}
180
198
  </div>
181
199
  '''
182
200
 
183
- # Build HTML sections
184
- queue_html = ''.join(render_queue_item(item) for item in
185
- quasarr_queue) if quasarr_queue else '<p class="empty-message">No Quasarr packages in queue</p>'
186
- history_html = ''.join(render_history_item(item) for item in
187
- quasarr_history) if quasarr_history else '<p class="empty-message">No Quasarr packages in history</p>'
201
+ # Build queue section
202
+ queue_html = ''
203
+ if quasarr_queue:
204
+ queue_items = ''.join(render_queue_item(item) for item in quasarr_queue)
205
+ queue_html = f'''
206
+ <div class="section">
207
+ <h3>⬇️ Downloading</h3>
208
+ <div class="packages-list">{queue_items}</div>
209
+ </div>
210
+ '''
211
+ else:
212
+ queue_html = '<div class="section"><p class="empty-message">No active downloads</p></div>'
213
+
214
+ # Build history section
215
+ history_html = ''
216
+ if quasarr_history:
217
+ history_items = ''.join(render_history_item(item) for item in quasarr_history[:10])
218
+ history_html = f'''
219
+ <div class="section">
220
+ <h3>📜 Recent History</h3>
221
+ <div class="packages-list">{history_items}</div>
222
+ </div>
223
+ '''
188
224
 
225
+ # Build "other packages" section (non-Quasarr)
189
226
  other_html = ''
190
- if other_queue or other_history:
191
- other_count = len(other_queue) + len(other_history)
227
+ other_count = len(other_queue) + len(other_history)
228
+ if other_count > 0:
192
229
  other_items = ''
193
230
  if other_queue:
194
- other_items += '<h4>⏳ Queue</h4>' + ''.join(render_queue_item(item) for item in other_queue)
231
+ other_items += f'<h4>Queue ({len(other_queue)})</h4>'
232
+ other_items += ''.join(render_queue_item(item) for item in other_queue)
195
233
  if other_history:
196
- other_items += '<h4>📜 History</h4>' + ''.join(render_history_item(item) for item in other_history)
234
+ other_items += f'<h4>History ({len(other_history)})</h4>'
235
+ other_items += ''.join(render_history_item(item) for item in other_history[:5])
236
+
197
237
  plural = 's' if other_count != 1 else ''
198
238
  other_html = f'''
199
239
  <div class="other-packages-section">
@@ -204,36 +244,30 @@ def setup_packages_routes(app):
204
244
  </div>
205
245
  '''
206
246
 
207
- queue_extra = f" + {len(other_queue)} other" if other_queue else ""
208
- history_extra = f" + {len(other_history)} other" if other_history else ""
209
-
210
- refresh_btn = render_button("Refresh Now", "primary", {"onclick": "location.reload()"})
211
247
  back_btn = render_button("Back", "secondary", {"onclick": "location.href='/'"})
212
248
 
213
249
  packages_html = f'''
214
250
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
215
- <h2>📦 Package Status</h2>
216
- <div class="refresh-indicator"><span id="countdown">30</span>s until refresh</div>
251
+ <h2>Packages</h2>
252
+
253
+ {status_message}
254
+
255
+ <div class="refresh-indicator" onclick="location.reload()">
256
+ Auto-refresh in <span id="countdown">10</span>s
257
+ </div>
217
258
 
218
259
  <div class="packages-container">
219
- <div class="section">
220
- <h3>⏳ Queue ({len(quasarr_queue)}{queue_extra})</h3>
221
- <div class="packages-list">{queue_html}</div>
222
- </div>
223
- <div class="section">
224
- <h3>📜 History ({len(quasarr_history)}{history_extra})</h3>
225
- <div class="packages-list">{history_html}</div>
226
- </div>
260
+ {queue_html}
261
+ {history_html}
227
262
  {other_html}
228
263
  </div>
229
264
 
230
- <p>{refresh_btn}</p>
231
265
  <p>{back_btn}</p>
232
266
 
233
- <!-- Delete Confirmation Modal -->
234
- <div id="deleteModal" class="modal">
267
+ <!-- Delete confirmation modal -->
268
+ <div class="modal" id="deleteModal">
235
269
  <div class="modal-content">
236
- <h3>⚠️ Delete Package?</h3>
270
+ <h3>🗑️ Delete Package?</h3>
237
271
  <p class="modal-package-name" id="modalPackageName"></p>
238
272
  <div class="modal-warning">
239
273
  <strong>⛔ Warning:</strong> This will permanently delete the package AND all associated files from disk. This action cannot be undone!
@@ -289,7 +323,8 @@ def setup_packages_routes(app):
289
323
  .btn-small.danger:hover {{ background: var(--btn-danger-hover-bg, #dc3545); color: white; }}
290
324
 
291
325
  .empty-message {{ color: var(--text-muted, #888); font-style: italic; text-align: center; padding: 20px; }}
292
- .refresh-indicator {{ text-align: center; font-size: 0.85em; color: var(--text-muted, #888); margin-bottom: 15px; }}
326
+ .refresh-indicator {{ text-align: center; font-size: 0.85em; color: var(--text-muted, #888); margin-bottom: 15px; cursor: pointer; }}
327
+ .refresh-indicator:hover {{ color: var(--link-color, #0066cc); text-decoration: underline; }}
293
328
 
294
329
  .other-packages-section {{ margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color, #ddd); }}
295
330
  .other-packages-section summary {{ cursor: pointer; padding: 8px 0; color: var(--text-muted, #666); }}
@@ -297,12 +332,30 @@ def setup_packages_routes(app):
297
332
  .other-packages-content {{ margin-top: 15px; }}
298
333
  .other-packages-content h4 {{ margin: 15px 0 10px 0; font-size: 0.95em; color: var(--text-muted, #666); }}
299
334
 
335
+ /* Status message styling */
336
+ .status-message {{
337
+ padding: 10px 15px;
338
+ border-radius: 6px;
339
+ margin-bottom: 15px;
340
+ font-weight: 500;
341
+ }}
342
+ .status-message.success {{
343
+ background: var(--success-bg, #d1e7dd);
344
+ color: var(--success-color, #198754);
345
+ border: 1px solid var(--success-border, #a3cfbb);
346
+ }}
347
+ .status-message.error {{
348
+ background: var(--error-bg, #f8d7da);
349
+ color: var(--error-color, #dc3545);
350
+ border: 1px solid var(--error-border, #f1aeb5);
351
+ }}
352
+
300
353
  /* Modal */
301
354
  .modal {{ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }}
302
355
  .modal.show {{ display: flex; }}
303
356
  .modal-content {{ background: var(--modal-bg, white); padding: 25px; border-radius: 12px; max-width: 400px; width: 90%; text-align: center; }}
304
357
  .modal-content h3 {{ margin: 0 0 15px 0; color: var(--error-msg-color, #c62828); }}
305
- .modal-package-name {{ font-weight: 500; word-break: break-word; padding: 10px; background: var(--card-bg, #f5f5f5); border-radius: 6px; margin: 10px 0; }}
358
+ .modal-package-name {{ font-weight: 500; word-break: break-word; padding: 10px; background: var(--code-bg, #f5f5f5); border-radius: 6px; margin: 10px 0; }}
306
359
  .modal-warning {{ background: var(--error-msg-bg, #ffebee); color: var(--error-msg-color, #c62828); padding: 12px; border-radius: 6px; margin: 15px 0; font-size: 0.9em; text-align: left; }}
307
360
  .modal-buttons {{ display: flex; gap: 10px; justify-content: center; margin-top: 20px; }}
308
361
  .btn-danger {{ background: var(--btn-danger-bg, #dc3545); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 500; }}
@@ -314,19 +367,20 @@ def setup_packages_routes(app):
314
367
  --card-bg: #2d3748; --card-border: #4a5568; --card-shadow: rgba(0,0,0,0.3);
315
368
  --border-color: #4a5568; --text-muted: #a0aec0;
316
369
  --progress-track: #4a5568; --progress-fill: #68d391;
317
- --error-border: #fc8181; --error-bg: #2d2d2d; --error-msg-bg: #3d2d2d; --error-msg-color: #fc8181;
370
+ --error-border: #fc8181; --error-bg: #3d2d2d; --error-msg-bg: #3d2d2d; --error-msg-color: #fc8181;
318
371
  --badge-archive-bg: #1a365d; --badge-archive-color: #63b3ed;
319
372
  --badge-success-bg: #1c4532; --badge-success-color: #68d391;
320
373
  --badge-warning-bg: #3d2d1a; --badge-warning-color: #f6ad55;
321
- --link-color: #63b3ed; --modal-bg: #2d3748;
374
+ --link-color: #63b3ed; --modal-bg: #2d3748; --code-bg: #1a202c;
322
375
  --btn-primary-bg: #3182ce; --btn-primary-hover: #2c5282;
323
376
  --btn-danger-text: #fc8181; --btn-danger-border: #fc8181; --btn-danger-hover-bg: #e53e3e;
377
+ --success-bg: #1c4532; --success-color: #68d391; --success-border: #276749;
324
378
  }}
325
379
  }}
326
380
  </style>
327
381
 
328
382
  <script>
329
- let countdown = 30;
383
+ let countdown = 10;
330
384
  const countdownEl = document.getElementById('countdown');
331
385
  const refreshInterval = setInterval(() => {{
332
386
  countdown--;
@@ -334,6 +388,13 @@ def setup_packages_routes(app):
334
388
  if (countdown <= 0) location.reload();
335
389
  }}, 1000);
336
390
 
391
+ // Clear status message from URL after display
392
+ if (window.location.search.includes('deleted=')) {{
393
+ const url = new URL(window.location);
394
+ url.searchParams.delete('deleted');
395
+ window.history.replaceState({{}}, '', url);
396
+ }}
397
+
337
398
  // Restore collapse state from localStorage
338
399
  const otherDetails = document.getElementById('otherPackagesDetails');
339
400
  const otherSummary = document.getElementById('otherPackagesSummary');
@@ -8,13 +8,21 @@ from bottle import request, abort
8
8
 
9
9
  from quasarr.downloads import fail
10
10
  from quasarr.providers import shared_state
11
+ from quasarr.providers.auth import require_api_key
11
12
  from quasarr.providers.log import info
12
13
  from quasarr.providers.notifications import send_discord_message
13
14
  from quasarr.providers.statistics import StatsHelper
14
15
 
15
16
 
16
17
  def setup_sponsors_helper_routes(app):
18
+ @app.get("/sponsors_helper/api/ping/")
19
+ @require_api_key
20
+ def ping_api():
21
+ """Health check endpoint for SponsorsHelper to verify connectivity."""
22
+ return "pong"
23
+
17
24
  @app.get("/sponsors_helper/api/to_decrypt/")
25
+ @require_api_key
18
26
  def to_decrypt_api():
19
27
  try:
20
28
  if not shared_state.values["helper_active"]:
@@ -60,6 +68,7 @@ def setup_sponsors_helper_routes(app):
60
68
  return abort(500, str(e))
61
69
 
62
70
  @app.post("/sponsors_helper/api/to_download/")
71
+ @require_api_key
63
72
  def to_download_api():
64
73
  try:
65
74
  data = request.json
@@ -86,9 +95,10 @@ def setup_sponsors_helper_routes(app):
86
95
  info(f"Error decrypting: {e}")
87
96
 
88
97
  StatsHelper(shared_state).increment_failed_decryptions_automatic()
89
- return abort(500, "Failed") #
98
+ return abort(500, "Failed")
90
99
 
91
100
  @app.post("/sponsors_helper/api/to_replace/")
101
+ @require_api_key
92
102
  def to_replace_api():
93
103
  try:
94
104
  data = request.json
@@ -130,6 +140,7 @@ def setup_sponsors_helper_routes(app):
130
140
  return {"error": str(e)}, 500
131
141
 
132
142
  @app.delete("/sponsors_helper/api/to_failed/")
143
+ @require_api_key
133
144
  def move_to_failed_api():
134
145
  try:
135
146
  StatsHelper(shared_state).increment_failed_decryptions_automatic()
@@ -153,6 +164,7 @@ def setup_sponsors_helper_routes(app):
153
164
  return abort(500, "Failed")
154
165
 
155
166
  @app.put("/sponsors_helper/api/set_sponsor_status/")
167
+ @require_api_key
156
168
  def activate_sponsor_status():
157
169
  try:
158
170
  data = request.body.read().decode("utf-8")
@@ -164,7 +176,3 @@ def setup_sponsors_helper_routes(app):
164
176
  except:
165
177
  pass
166
178
  return abort(500, "Failed")
167
-
168
- @app.get("/sponsors_helper/api/ping/")
169
- def get_sponsor_status():
170
- return "pong"