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 +4 -4
- quasarr/api/arr/__init__.py +55 -33
- quasarr/api/config/__init__.py +1 -1
- quasarr/api/packages/__init__.py +115 -54
- quasarr/api/sponsors_helper/__init__.py +13 -5
- quasarr/downloads/__init__.py +117 -6
- quasarr/downloads/linkcrypters/filecrypt.py +1 -1
- quasarr/providers/auth.py +16 -1
- quasarr/providers/html_templates.py +65 -10
- quasarr/providers/version.py +1 -1
- quasarr/search/sources/al.py +1 -1
- quasarr/search/sources/by.py +1 -1
- quasarr/search/sources/dd.py +2 -1
- quasarr/search/sources/dj.py +2 -2
- quasarr/search/sources/dl.py +8 -2
- quasarr/search/sources/dt.py +1 -1
- quasarr/search/sources/dw.py +6 -7
- quasarr/search/sources/fx.py +4 -4
- quasarr/search/sources/he.py +1 -1
- quasarr/search/sources/mb.py +1 -1
- quasarr/search/sources/nk.py +1 -1
- quasarr/search/sources/nx.py +1 -1
- quasarr/search/sources/sf.py +4 -2
- quasarr/search/sources/sj.py +2 -2
- quasarr/search/sources/sl.py +3 -3
- quasarr/search/sources/wd.py +1 -1
- quasarr/search/sources/wx.py +4 -3
- {quasarr-2.0.0.dist-info → quasarr-2.1.1.dist-info}/METADATA +72 -43
- {quasarr-2.0.0.dist-info → quasarr-2.1.1.dist-info}/RECORD +33 -33
- {quasarr-2.0.0.dist-info → quasarr-2.1.1.dist-info}/WHEEL +0 -0
- {quasarr-2.0.0.dist-info → quasarr-2.1.1.dist-info}/entry_points.txt +0 -0
- {quasarr-2.0.0.dist-info → quasarr-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {quasarr-2.0.0.dist-info → quasarr-2.1.1.dist-info}/top_level.txt +0 -0
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-
|
|
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 = '<
|
|
393
|
-
return render_centered_html(info
|
|
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():
|
quasarr/api/arr/__init__.py
CHANGED
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
193
|
-
imdb_id
|
|
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,
|
|
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>
|
quasarr/api/config/__init__.py
CHANGED
quasarr/api/packages/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
{
|
|
197
|
+
{actions}
|
|
180
198
|
</div>
|
|
181
199
|
'''
|
|
182
200
|
|
|
183
|
-
# Build
|
|
184
|
-
queue_html = ''
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
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
|
|
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
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
|
234
|
-
<div
|
|
267
|
+
<!-- Delete confirmation modal -->
|
|
268
|
+
<div class="modal" id="deleteModal">
|
|
235
269
|
<div class="modal-content">
|
|
236
|
-
<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(--
|
|
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: #
|
|
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 =
|
|
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"
|