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 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 += f"""
38
- <div class="section">
39
- <h2>🔒 Link{plural} waiting for CAPTCHA solution</h2>
40
- """
41
-
42
- if not shared_state.values.get("helper_active"):
43
- captcha_hint += f"""
44
- <p>
45
- <a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" target="_blank">
46
- Sponsors get automated CAPTCHA solutions!
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="section">
64
- <h2>📖 Setup Instructions</h2>
65
- <p>
66
- <a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank">
67
- Refer to the README for detailed instructions.
68
- </a>
69
- </p>
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">Show API Settings</summary>
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
- <h3>URL</h3>
83
- <div class="url-wrapper">
84
- <input id="urlInput" class="copy-input" type="text" readonly value="{shared_state.values['internal_address']}" />
85
- <button id="copyUrl" class="btn-primary small">Copy</button>
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
- <h3>API Key</h3>
89
- <div class="api-key-wrapper">
90
- <input id="apiKeyInput" class="copy-input" type="password" readonly value="{api_key}" />
91
- <button id="toggleKey" class="btn-secondary small">Show</button>
92
- <button id="copyKey" class="btn-primary small">Copy</button>
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>{render_button("Regenerate API key", "secondary", {"onclick": "if(confirm('Regenerate API key?')) location.href='/regenerate-api-key';"})}</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
- <hr>
101
-
102
- <div class="section">
103
- <h2>🔧 Quick Actions</h2>
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
- .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;
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
- summary:hover {{
120
- color: #0066cc;
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
- 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
-
137
- const apiInput = document.getElementById('apiKeyInput');
138
- const toggleBtn = document.getElementById('toggleKey');
139
- const copyBtn = document.getElementById('copyKey');
140
-
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
- }}
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
- return render_centered_html(info)
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():
@@ -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
- package = protected[0]
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