quasarr 1.31.0__py3-none-any.whl → 2.0.0__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 +324 -106
- quasarr/api/captcha/__init__.py +26 -1
- quasarr/api/packages/__init__.py +374 -0
- quasarr/api/sponsors_helper/__init__.py +4 -0
- quasarr/downloads/__init__.py +2 -0
- quasarr/downloads/linkcrypters/hide.py +45 -6
- quasarr/downloads/packages/__init__.py +482 -219
- quasarr/providers/auth.py +250 -0
- quasarr/providers/jd_cache.py +211 -53
- quasarr/providers/obfuscated.py +9 -7
- quasarr/providers/shared_state.py +24 -0
- quasarr/providers/version.py +1 -1
- quasarr/search/sources/dl.py +3 -2
- quasarr/storage/setup.py +15 -1
- {quasarr-1.31.0.dist-info → quasarr-2.0.0.dist-info}/METADATA +12 -2
- {quasarr-1.31.0.dist-info → quasarr-2.0.0.dist-info}/RECORD +20 -18
- {quasarr-1.31.0.dist-info → quasarr-2.0.0.dist-info}/WHEEL +0 -0
- {quasarr-1.31.0.dist-info → quasarr-2.0.0.dist-info}/entry_points.txt +0 -0
- {quasarr-1.31.0.dist-info → quasarr-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.31.0.dist-info → quasarr-2.0.0.dist-info}/top_level.txt +0 -0
quasarr/api/__init__.py
CHANGED
|
@@ -8,9 +8,11 @@ import quasarr.providers.html_images as images
|
|
|
8
8
|
from quasarr.api.arr import setup_arr_routes
|
|
9
9
|
from quasarr.api.captcha import setup_captcha_routes
|
|
10
10
|
from quasarr.api.config import setup_config
|
|
11
|
+
from quasarr.api.packages import setup_packages_routes
|
|
11
12
|
from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
12
13
|
from quasarr.api.statistics import setup_statistics
|
|
13
14
|
from quasarr.providers import shared_state
|
|
15
|
+
from quasarr.providers.auth import add_auth_routes, add_auth_hook, show_logout_link
|
|
14
16
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
15
17
|
from quasarr.providers.web_server import Server
|
|
16
18
|
from quasarr.storage.config import Config
|
|
@@ -21,158 +23,374 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
21
23
|
|
|
22
24
|
app = Bottle()
|
|
23
25
|
|
|
26
|
+
# Auth: routes must come first, then hook
|
|
27
|
+
add_auth_routes(app)
|
|
28
|
+
add_auth_hook(app, whitelist_prefixes=['/api', '/api/' '/sponsors_helper/', '/download/'])
|
|
29
|
+
|
|
24
30
|
setup_arr_routes(app)
|
|
25
31
|
setup_captcha_routes(app)
|
|
26
32
|
setup_config(app, shared_state)
|
|
27
33
|
setup_statistics(app, shared_state)
|
|
28
34
|
setup_sponsors_helper_routes(app)
|
|
35
|
+
setup_packages_routes(app)
|
|
29
36
|
|
|
30
37
|
@app.get('/')
|
|
31
38
|
def index():
|
|
32
39
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
33
40
|
api_key = Config('API').get('key')
|
|
41
|
+
|
|
42
|
+
# Get quick status summary
|
|
43
|
+
try:
|
|
44
|
+
device = shared_state.values.get("device")
|
|
45
|
+
jd_connected = device is not None
|
|
46
|
+
except:
|
|
47
|
+
jd_connected = False
|
|
48
|
+
|
|
49
|
+
# CAPTCHA banner
|
|
34
50
|
captcha_hint = ""
|
|
35
51
|
if protected:
|
|
36
52
|
plural = 's' if len(protected) > 1 else ''
|
|
37
|
-
captcha_hint
|
|
38
|
-
<div class="
|
|
39
|
-
<
|
|
40
|
-
""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</a>
|
|
48
|
-
</p>
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
plural = 's' if len(protected) > 1 else ''
|
|
52
|
-
captcha_hint += f"""
|
|
53
|
-
<p>{render_button(f"Solve CAPTCHA{plural}", 'primary', {'onclick': "location.href='/captcha'"})}</p>
|
|
53
|
+
captcha_hint = f"""
|
|
54
|
+
<div class="alert alert-warning">
|
|
55
|
+
<span class="alert-icon">🔒</span>
|
|
56
|
+
<div class="alert-content">
|
|
57
|
+
<strong>{len(protected)} link{plural} waiting for CAPTCHA</strong>
|
|
58
|
+
{"" if shared_state.values.get("helper_active") else '<br><a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" target="_blank">Sponsors get automated CAPTCHA solutions!</a>'}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="alert-action">
|
|
61
|
+
{render_button(f"Solve CAPTCHA{plural}", 'primary', {'onclick': "location.href='/captcha'"})}
|
|
62
|
+
</div>
|
|
54
63
|
</div>
|
|
55
|
-
<hr>
|
|
56
64
|
"""
|
|
57
65
|
|
|
66
|
+
# JDownloader status
|
|
67
|
+
jd_status = f"""
|
|
68
|
+
<div class="status-bar">
|
|
69
|
+
<span class="status-item {'status-ok' if jd_connected else 'status-error'}">
|
|
70
|
+
{'✅' if jd_connected else '❌'} JDownloader {'Connected' if jd_connected else 'Disconnected'}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
"""
|
|
74
|
+
|
|
58
75
|
info = f"""
|
|
59
76
|
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
60
77
|
|
|
78
|
+
{jd_status}
|
|
61
79
|
{captcha_hint}
|
|
62
80
|
|
|
63
|
-
<div class="
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
<div class="quick-actions">
|
|
82
|
+
<a href="/packages" class="action-card">
|
|
83
|
+
<span class="action-icon">📦</span>
|
|
84
|
+
<span class="action-label">Packages</span>
|
|
85
|
+
</a>
|
|
86
|
+
<a href="/statistics" class="action-card">
|
|
87
|
+
<span class="action-icon">📊</span>
|
|
88
|
+
<span class="action-label">Statistics</span>
|
|
89
|
+
</a>
|
|
90
|
+
<a href="/hostnames" class="action-card">
|
|
91
|
+
<span class="action-icon">🌐</span>
|
|
92
|
+
<span class="action-label">Hostnames</span>
|
|
93
|
+
</a>
|
|
94
|
+
<a href="/flaresolverr" class="action-card">
|
|
95
|
+
<span class="action-icon">🛡️</span>
|
|
96
|
+
<span class="action-label">FlareSolverr</span>
|
|
97
|
+
</a>
|
|
70
98
|
</div>
|
|
71
99
|
|
|
72
|
-
<hr>
|
|
73
|
-
|
|
74
100
|
<div class="section">
|
|
75
|
-
<h2>⚙️ API Configuration</h2>
|
|
76
|
-
<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>
|
|
77
|
-
|
|
78
101
|
<details id="apiDetails">
|
|
79
|
-
<summary id="apiSummary"
|
|
102
|
+
<summary id="apiSummary">⚙️ API Configuration</summary>
|
|
80
103
|
<div class="api-settings">
|
|
104
|
+
<p class="api-hint">Use these settings for <strong>Newznab Indexer</strong> and <strong>SABnzbd Download Client</strong> in Radarr/Sonarr</p>
|
|
81
105
|
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
<div class="input-group">
|
|
107
|
+
<label>URL</label>
|
|
108
|
+
<div class="input-row">
|
|
109
|
+
<input id="urlInput" type="text" readonly value="{shared_state.values['internal_address']}" />
|
|
110
|
+
<button id="copyUrl" type="button">Copy</button>
|
|
111
|
+
</div>
|
|
86
112
|
</div>
|
|
87
113
|
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
<div class="input-group">
|
|
115
|
+
<label>API Key</label>
|
|
116
|
+
<div class="input-row">
|
|
117
|
+
<input id="apiKeyInput" type="password" readonly value="{api_key}" />
|
|
118
|
+
<button id="toggleKey" type="button">Show</button>
|
|
119
|
+
<button id="copyKey" type="button">Copy</button>
|
|
120
|
+
</div>
|
|
93
121
|
</div>
|
|
94
122
|
|
|
95
|
-
<p
|
|
123
|
+
<p style="margin-top: 15px;">
|
|
124
|
+
{render_button("Regenerate API key", "secondary", {"onclick": "if(confirm('Regenerate API key?')) location.href='/regenerate-api-key';"})}
|
|
125
|
+
</p>
|
|
96
126
|
</div>
|
|
97
127
|
</details>
|
|
98
128
|
</div>
|
|
99
129
|
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<p><button class="btn-primary" onclick="location.href='/hostnames'">Update Hostnames</button></p>
|
|
105
|
-
<p><button class="btn-primary" onclick="location.href='/flaresolverr'">Configure FlareSolverr</button></p>
|
|
106
|
-
<p><button class="btn-primary" onclick="location.href='/statistics'">View Statistics</button></p>
|
|
130
|
+
<div class="section help-link">
|
|
131
|
+
<a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank">
|
|
132
|
+
📖 Setup Instructions & Documentation
|
|
133
|
+
</a>
|
|
107
134
|
</div>
|
|
108
135
|
|
|
109
136
|
<style>
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
.status-bar {{
|
|
138
|
+
display: flex;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
gap: 20px;
|
|
141
|
+
margin-bottom: 20px;
|
|
142
|
+
flex-wrap: wrap;
|
|
143
|
+
}}
|
|
144
|
+
.status-item {{
|
|
145
|
+
font-size: 0.9em;
|
|
146
|
+
padding: 6px 12px;
|
|
147
|
+
border-radius: 20px;
|
|
148
|
+
background: var(--status-bg, #f5f5f5);
|
|
149
|
+
}}
|
|
150
|
+
.status-ok {{ color: var(--status-ok, #2e7d32); }}
|
|
151
|
+
.status-error {{ color: var(--status-error, #c62828); }}
|
|
152
|
+
|
|
153
|
+
.alert {{
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
align-items: center;
|
|
157
|
+
text-align: center;
|
|
158
|
+
gap: 12px;
|
|
159
|
+
padding: 20px;
|
|
160
|
+
border-radius: 8px;
|
|
161
|
+
margin-bottom: 25px;
|
|
162
|
+
}}
|
|
163
|
+
.alert-warning {{
|
|
164
|
+
background: var(--alert-warning-bg, #fff3e0);
|
|
165
|
+
border: 1px solid var(--alert-warning-border, #ffb74d);
|
|
166
|
+
}}
|
|
167
|
+
.alert-icon {{ font-size: 1.5em; }}
|
|
168
|
+
.alert-content {{ }}
|
|
169
|
+
.alert-content a {{ color: var(--link-color, #0066cc); }}
|
|
170
|
+
.alert-action {{ margin-top: 5px; }}
|
|
171
|
+
|
|
172
|
+
.quick-actions {{
|
|
173
|
+
display: grid;
|
|
174
|
+
grid-template-columns: repeat(4, 1fr);
|
|
175
|
+
gap: 12px;
|
|
176
|
+
max-width: 500px;
|
|
177
|
+
margin: 0 auto 30px auto;
|
|
178
|
+
}}
|
|
179
|
+
@media (max-width: 500px) {{
|
|
180
|
+
.quick-actions {{
|
|
181
|
+
grid-template-columns: repeat(2, 1fr);
|
|
182
|
+
}}
|
|
183
|
+
}}
|
|
184
|
+
.action-card {{
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
align-items: center;
|
|
188
|
+
padding: 15px 10px;
|
|
189
|
+
background: var(--card-bg, #f8f9fa);
|
|
190
|
+
border: 1px solid var(--card-border, #dee2e6);
|
|
191
|
+
border-radius: 10px;
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
color: inherit;
|
|
194
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
195
|
+
}}
|
|
196
|
+
.action-card:hover {{
|
|
197
|
+
transform: translateY(-2px);
|
|
198
|
+
box-shadow: 0 4px 12px var(--card-shadow, rgba(0,0,0,0.1));
|
|
199
|
+
border-color: var(--card-hover-border, #007bff);
|
|
200
|
+
}}
|
|
201
|
+
.action-icon {{ font-size: 1.8em; margin-bottom: 5px; }}
|
|
202
|
+
.action-label {{ font-size: 0.85em; font-weight: 500; }}
|
|
203
|
+
|
|
204
|
+
.section {{ margin: 20px 0; max-width: 500px; margin-left: auto; margin-right: auto; }}
|
|
205
|
+
details {{ background: var(--card-bg, #f8f9fa); border: 1px solid var(--card-border, #dee2e6); border-radius: 8px; }}
|
|
206
|
+
summary {{
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
padding: 12px 15px;
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
list-style: none;
|
|
211
|
+
}}
|
|
212
|
+
summary::-webkit-details-marker {{ display: none; }}
|
|
213
|
+
summary::before {{ content: '▶ '; font-size: 0.8em; }}
|
|
214
|
+
details[open] summary::before {{ content: '▼ '; }}
|
|
215
|
+
summary:hover {{ color: var(--link-color, #0066cc); }}
|
|
216
|
+
|
|
217
|
+
.api-settings {{ padding: 15px; border-top: 1px solid var(--card-border, #dee2e6); }}
|
|
218
|
+
.api-hint {{ font-size: 0.9em; color: var(--text-muted, #666); margin-bottom: 15px; }}
|
|
219
|
+
.input-group {{ margin-bottom: 15px; }}
|
|
220
|
+
.input-group label {{ display: block; font-weight: 500; margin-bottom: 6px; font-size: 0.95em; text-align: left; }}
|
|
221
|
+
.input-row {{
|
|
222
|
+
display: flex;
|
|
223
|
+
gap: 8px;
|
|
224
|
+
align-items: stretch;
|
|
225
|
+
}}
|
|
226
|
+
.input-row input {{
|
|
227
|
+
flex: 1;
|
|
228
|
+
padding: 8px 12px;
|
|
229
|
+
border: 1px solid var(--input-border, #ced4da);
|
|
230
|
+
border-radius: 4px;
|
|
231
|
+
font-family: monospace;
|
|
232
|
+
font-size: 0.9em;
|
|
233
|
+
background: var(--input-bg, #e9ecef);
|
|
234
|
+
color: var(--fg-color, #212529);
|
|
235
|
+
min-width: 0;
|
|
236
|
+
margin: 0;
|
|
237
|
+
}}
|
|
238
|
+
.input-row button {{
|
|
239
|
+
padding: 8px 16px;
|
|
240
|
+
border: none;
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
font-size: 0.9em;
|
|
244
|
+
font-weight: 500;
|
|
245
|
+
transition: background 0.2s;
|
|
246
|
+
white-space: nowrap;
|
|
247
|
+
margin: 0;
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
}}
|
|
250
|
+
#copyUrl, #copyKey {{
|
|
251
|
+
background: var(--btn-primary-bg, #007bff);
|
|
252
|
+
color: white;
|
|
253
|
+
}}
|
|
254
|
+
#copyUrl:hover, #copyKey:hover {{
|
|
255
|
+
background: var(--btn-primary-hover, #0056b3);
|
|
256
|
+
}}
|
|
257
|
+
#toggleKey {{
|
|
258
|
+
background: var(--btn-secondary-bg, #6c757d);
|
|
259
|
+
color: white;
|
|
260
|
+
}}
|
|
261
|
+
#toggleKey:hover {{
|
|
262
|
+
background: var(--btn-secondary-hover, #545b62);
|
|
263
|
+
}}
|
|
264
|
+
|
|
265
|
+
.help-link {{
|
|
266
|
+
text-align: center;
|
|
267
|
+
padding: 15px;
|
|
268
|
+
background: var(--card-bg, #f8f9fa);
|
|
269
|
+
border: 1px solid var(--card-border, #dee2e6);
|
|
270
|
+
border-radius: 8px;
|
|
271
|
+
}}
|
|
272
|
+
.help-link a {{
|
|
273
|
+
color: var(--link-color, #0066cc);
|
|
274
|
+
text-decoration: none;
|
|
117
275
|
font-weight: 500;
|
|
118
276
|
}}
|
|
119
|
-
|
|
120
|
-
|
|
277
|
+
.help-link a:hover {{ text-decoration: underline; }}
|
|
278
|
+
|
|
279
|
+
.logout-link {{
|
|
280
|
+
display: block;
|
|
281
|
+
text-align: center;
|
|
282
|
+
margin-top: 20px;
|
|
283
|
+
font-size: 0.85em;
|
|
284
|
+
}}
|
|
285
|
+
.logout-link a {{
|
|
286
|
+
color: var(--text-muted, #666);
|
|
287
|
+
text-decoration: none;
|
|
288
|
+
}}
|
|
289
|
+
.logout-link a:hover {{ text-decoration: underline; }}
|
|
290
|
+
|
|
291
|
+
/* Dark mode */
|
|
292
|
+
@media (prefers-color-scheme: dark) {{
|
|
293
|
+
:root {{
|
|
294
|
+
--status-bg: #2d3748;
|
|
295
|
+
--status-ok: #68d391;
|
|
296
|
+
--status-error: #fc8181;
|
|
297
|
+
--alert-warning-bg: #3d3520;
|
|
298
|
+
--alert-warning-border: #d69e2e;
|
|
299
|
+
--card-bg: #2d3748;
|
|
300
|
+
--card-border: #4a5568;
|
|
301
|
+
--card-shadow: rgba(0,0,0,0.3);
|
|
302
|
+
--card-hover-border: #63b3ed;
|
|
303
|
+
--text-muted: #a0aec0;
|
|
304
|
+
--link-color: #63b3ed;
|
|
305
|
+
--input-bg: #1a202c;
|
|
306
|
+
--input-border: #4a5568;
|
|
307
|
+
--btn-primary-bg: #3182ce;
|
|
308
|
+
--btn-primary-hover: #2c5282;
|
|
309
|
+
--btn-secondary-bg: #4a5568;
|
|
310
|
+
--btn-secondary-hover: #2d3748;
|
|
311
|
+
}}
|
|
121
312
|
}}
|
|
122
313
|
</style>
|
|
123
314
|
|
|
124
315
|
<script>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
316
|
+
(function() {{
|
|
317
|
+
var urlInput = document.getElementById('urlInput');
|
|
318
|
+
var copyUrlBtn = document.getElementById('copyUrl');
|
|
319
|
+
var apiInput = document.getElementById('apiKeyInput');
|
|
320
|
+
var toggleBtn = document.getElementById('toggleKey');
|
|
321
|
+
var copyKeyBtn = document.getElementById('copyKey');
|
|
322
|
+
|
|
323
|
+
function copyToClipboard(text, button, callback) {{
|
|
324
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {{
|
|
325
|
+
navigator.clipboard.writeText(text).then(function() {{
|
|
326
|
+
var originalText = button.innerText;
|
|
327
|
+
button.innerText = 'Copied!';
|
|
328
|
+
setTimeout(function() {{
|
|
329
|
+
button.innerText = originalText;
|
|
330
|
+
if (callback) callback();
|
|
331
|
+
}}, 1500);
|
|
332
|
+
}}).catch(function() {{
|
|
333
|
+
fallbackCopy(text, button, callback);
|
|
334
|
+
}});
|
|
335
|
+
}} else {{
|
|
336
|
+
fallbackCopy(text, button, callback);
|
|
337
|
+
}}
|
|
338
|
+
}}
|
|
339
|
+
|
|
340
|
+
function fallbackCopy(text, button, callback) {{
|
|
341
|
+
var textarea = document.createElement('textarea');
|
|
342
|
+
textarea.value = text;
|
|
343
|
+
textarea.style.position = 'fixed';
|
|
344
|
+
textarea.style.opacity = '0';
|
|
345
|
+
document.body.appendChild(textarea);
|
|
346
|
+
textarea.select();
|
|
347
|
+
try {{
|
|
348
|
+
document.execCommand('copy');
|
|
349
|
+
var originalText = button.innerText;
|
|
350
|
+
button.innerText = 'Copied!';
|
|
351
|
+
setTimeout(function() {{
|
|
352
|
+
button.innerText = originalText;
|
|
353
|
+
if (callback) callback();
|
|
354
|
+
}}, 1500);
|
|
355
|
+
}} catch (e) {{
|
|
356
|
+
alert('Copy failed. Please copy manually.');
|
|
357
|
+
}}
|
|
358
|
+
document.body.removeChild(textarea);
|
|
359
|
+
}}
|
|
360
|
+
|
|
361
|
+
if (copyUrlBtn) {{
|
|
362
|
+
copyUrlBtn.onclick = function() {{
|
|
363
|
+
copyToClipboard(urlInput.value, copyUrlBtn);
|
|
364
|
+
}};
|
|
365
|
+
}}
|
|
366
|
+
|
|
367
|
+
if (copyKeyBtn) {{
|
|
368
|
+
copyKeyBtn.onclick = function() {{
|
|
369
|
+
copyToClipboard(apiInput.value, copyKeyBtn, function() {{
|
|
370
|
+
// Re-hide the API key after copying
|
|
371
|
+
apiInput.type = 'password';
|
|
372
|
+
toggleBtn.innerText = 'Show';
|
|
373
|
+
}});
|
|
374
|
+
}};
|
|
375
|
+
}}
|
|
376
|
+
|
|
377
|
+
if (toggleBtn) {{
|
|
378
|
+
toggleBtn.onclick = function() {{
|
|
379
|
+
if (apiInput.type === 'password') {{
|
|
380
|
+
apiInput.type = 'text';
|
|
381
|
+
toggleBtn.innerText = 'Hide';
|
|
382
|
+
}} else {{
|
|
383
|
+
apiInput.type = 'password';
|
|
384
|
+
toggleBtn.innerText = 'Show';
|
|
385
|
+
}}
|
|
386
|
+
}};
|
|
387
|
+
}}
|
|
388
|
+
}})();
|
|
173
389
|
</script>
|
|
174
390
|
"""
|
|
175
|
-
|
|
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)
|
|
176
394
|
|
|
177
395
|
@app.get('/regenerate-api-key')
|
|
178
396
|
def regenerate_api_key():
|
quasarr/api/captcha/__init__.py
CHANGED
|
@@ -61,7 +61,21 @@ def setup_captcha_routes(app):
|
|
|
61
61
|
{render_button("Confirm", "secondary", {"onclick": "location.href='/'"})}
|
|
62
62
|
</p>''')
|
|
63
63
|
else:
|
|
64
|
-
|
|
64
|
+
# Check if a specific package_id was requested
|
|
65
|
+
requested_package_id = request.query.get('package_id')
|
|
66
|
+
package = None
|
|
67
|
+
|
|
68
|
+
if requested_package_id:
|
|
69
|
+
# Find the specific package
|
|
70
|
+
for p in protected:
|
|
71
|
+
if p[0] == requested_package_id:
|
|
72
|
+
package = p
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
# Fall back to first package if not found or not specified
|
|
76
|
+
if package is None:
|
|
77
|
+
package = protected[0]
|
|
78
|
+
|
|
65
79
|
package_id = package[0]
|
|
66
80
|
data = json.loads(package[1])
|
|
67
81
|
title = data["title"]
|
|
@@ -1209,6 +1223,17 @@ def setup_captcha_routes(app):
|
|
|
1209
1223
|
border-right: none !important;
|
|
1210
1224
|
}
|
|
1211
1225
|
}
|
|
1226
|
+
/* Fix captcha container to shrink-wrap iframe on desktop */
|
|
1227
|
+
.captcha-container {
|
|
1228
|
+
display: inline-block;
|
|
1229
|
+
background-color: var(--secondary);
|
|
1230
|
+
}
|
|
1231
|
+
#puzzle-captcha {
|
|
1232
|
+
display: block;
|
|
1233
|
+
}
|
|
1234
|
+
#puzzle-captcha iframe {
|
|
1235
|
+
display: block;
|
|
1236
|
+
}
|
|
1212
1237
|
</style>
|
|
1213
1238
|
<script type="text/javascript">
|
|
1214
1239
|
// Package title for result display
|