quasarr 2.4.1__py3-none-any.whl → 2.4.5__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/__init__.py CHANGED
@@ -12,6 +12,7 @@ import time
12
12
 
13
13
  import requests
14
14
 
15
+ import quasarr.providers.web_server
15
16
  from quasarr.api import get_api
16
17
  from quasarr.providers import shared_state, version
17
18
  from quasarr.providers.log import info, debug
@@ -157,6 +158,8 @@ def run():
157
158
  skip_login_db = DataBase("skip_login")
158
159
  login_required_sites = ['al', 'dd', 'dl', 'nx']
159
160
 
161
+ quasarr.providers.web_server.temp_server_success = False
162
+
160
163
  for site in login_required_sites:
161
164
  hostname = Config('Hostnames').get(site)
162
165
  if hostname:
@@ -164,9 +167,12 @@ def run():
164
167
  user = site_config.get('user')
165
168
  password = site_config.get('password')
166
169
  if not user or not password:
167
- if skip_login_db.retrieve(site):
170
+ skip_val = skip_login_db.retrieve(site)
171
+ if skip_val and str(skip_val).lower() == "true":
168
172
  info(f'"{site.upper()}" login skipped by user preference')
169
173
  else:
174
+ info(f'"{site.upper()}" credentials missing. Launching setup...')
175
+ quasarr.providers.web_server.temp_server_success = False
170
176
  hostname_credentials_config(shared_state, site.upper(), hostname)
171
177
 
172
178
  # Check FlareSolverr configuration
@@ -257,7 +263,7 @@ def flaresolverr_checker(shared_state_dict, shared_state_lock):
257
263
  flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
258
264
 
259
265
  flaresolverr_url = Config('FlareSolverr').get('url')
260
-
266
+
261
267
  # If FlareSolverr is not configured and not skipped, it means it's the first run
262
268
  # and the user needs to be prompted via the WebUI.
263
269
  # This background process should NOT block or prompt the user.
@@ -265,7 +271,7 @@ def flaresolverr_checker(shared_state_dict, shared_state_lock):
265
271
  if not flaresolverr_url and not flaresolverr_skipped:
266
272
  info('FlareSolverr URL not configured. Please configure it via the WebUI.')
267
273
  info('Some sites (AL) will not work without FlareSolverr.')
268
- return # Exit the checker, it will be re-checked if user configures it later
274
+ return # Exit the checker, it will be re-checked if user configures it later
269
275
 
270
276
  if flaresolverr_skipped:
271
277
  info('FlareSolverr setup skipped by user preference')
quasarr/api/__init__.py CHANGED
@@ -47,7 +47,7 @@ def get_api(shared_state_dict, shared_state_lock):
47
47
  # Get quick status summary
48
48
  try:
49
49
  device = shared_state.values.get("device")
50
- jd_connected = device is not None
50
+ jd_connected = device is not None and device is not False
51
51
  except:
52
52
  jd_connected = False
53
53
 
@@ -67,8 +67,10 @@ def get_api(shared_state_dict, shared_state_lock):
67
67
  # Skip unset hostnames and skipped logins
68
68
  if not current_value:
69
69
  continue
70
- if shorthand in login_required_sites and skip_login_db.retrieve(shorthand):
71
- continue
70
+ if shorthand in login_required_sites:
71
+ skip_val = skip_login_db.retrieve(shorthand)
72
+ if skip_val and str(skip_val).lower() == "true":
73
+ continue
72
74
 
73
75
  # This hostname counts toward total
74
76
  total_count += 1
@@ -474,6 +476,7 @@ def get_api(shared_state_dict, shared_state_lock):
474
476
 
475
477
  @app.get('/regenerate-api-key')
476
478
  def regenerate_api_key():
479
+ shared_state.generate_api_key()
477
480
  return render_success(f'API key replaced!', 5)
478
481
 
479
482
  Server(app, listen='0.0.0.0', port=shared_state.values["port"]).serve_forever()
@@ -48,7 +48,11 @@ def setup_captcha_routes(app):
48
48
  device = None
49
49
  if not device:
50
50
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
51
- <p>JDownloader connection not established.</p>
51
+ <div class="status-bar">
52
+ <span class="status-pill error">
53
+ ❌ JDownloader disconnected
54
+ </span>
55
+ </div>
52
56
  <p>
53
57
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
54
58
  </p>''')
@@ -320,7 +324,7 @@ def setup_captcha_routes(app):
320
324
 
321
325
  source_button = ""
322
326
  if original_url:
323
- source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')" })}</p>'
327
+ source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
324
328
 
325
329
  return render_centered_html(f"""
326
330
  <!DOCTYPE html>
@@ -366,7 +370,7 @@ def setup_captcha_routes(app):
366
370
 
367
371
  source_button = ""
368
372
  if original_url:
369
- source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')" })}</p>'
373
+ source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
370
374
 
371
375
  return render_centered_html(f"""
372
376
  <!DOCTYPE html>
@@ -413,7 +417,7 @@ def setup_captcha_routes(app):
413
417
 
414
418
  source_button = ""
415
419
  if original_url:
416
- source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')" })}</p>'
420
+ source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
417
421
 
418
422
  return render_centered_html(f"""
419
423
  <!DOCTYPE html>
@@ -460,7 +464,7 @@ def setup_captcha_routes(app):
460
464
 
461
465
  source_button = ""
462
466
  if original_url:
463
- source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')" })}</p>'
467
+ source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
464
468
 
465
469
  return render_centered_html(f"""
466
470
  <!DOCTYPE html>
@@ -848,7 +852,7 @@ def setup_captcha_routes(app):
848
852
 
849
853
  source_button = ""
850
854
  if original_url:
851
- source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')" })}</p>'
855
+ source_button = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
852
856
 
853
857
  return render_centered_html(f"""
854
858
  <!DOCTYPE html>
@@ -1215,7 +1219,7 @@ def setup_captcha_routes(app):
1215
1219
 
1216
1220
  source_button_html = ""
1217
1221
  if original_url:
1218
- source_button_html = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')" })}</p>'
1222
+ source_button_html = f'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
1219
1223
 
1220
1224
  content = render_centered_html(r'''
1221
1225
  <style>
@@ -41,24 +41,30 @@ def _render_queue_item(item):
41
41
  cat = item.get('cat', 'not_quasarr')
42
42
  is_archive = item.get('is_archive', False)
43
43
  nzo_id = item.get('nzo_id', '')
44
+ storage = item.get('storage', '')
44
45
 
45
46
  is_captcha = '[CAPTCHA' in filename
47
+ status_text = 'Downloading'
46
48
  if is_captcha:
47
49
  status_emoji = '🔒'
50
+ status_text = 'Waiting for CAPTCHA Solution!'
48
51
  elif '[Extracting]' in filename:
49
52
  status_emoji = '📦'
53
+ status_text = 'Extracting'
50
54
  elif '[Paused]' in filename:
51
55
  status_emoji = '⏸️'
56
+ status_text = 'Paused'
52
57
  elif '[Linkgrabber]' in filename:
53
58
  status_emoji = '🔗'
59
+ status_text = 'Linkgrabber'
54
60
  else:
55
- status_emoji = '⬇️'
61
+ status_emoji = '▶️'
56
62
 
57
63
  display_name = filename
58
64
  for prefix in ['[Downloading] ', '[Extracting] ', '[Paused] ', '[Linkgrabber] ', '[CAPTCHA not solved!] ']:
59
65
  display_name = display_name.replace(prefix, '')
60
66
 
61
- archive_badge = '<span class="badge archive">📁</span>' if is_archive else ''
67
+ archive_badge = '📦' if is_archive else ''
62
68
  cat_emoji = _get_category_emoji(cat)
63
69
  size_str = _format_size(bytes_val=bytes_val) if bytes_val else _format_size(mb=mb)
64
70
 
@@ -68,23 +74,38 @@ def _render_queue_item(item):
68
74
  else:
69
75
  progress_html = f'<div class="progress-track"><div class="progress-fill" style="width: {percentage}%"></div></div>'
70
76
 
71
- # Action buttons - CAPTCHA left, delete right
77
+ # Interactive info
78
+ info_onclick = f"showPackageDetails('{nzo_id}', '{_escape_js(display_name)}', '{cat}', '{'Yes' if is_archive else 'No'}', '', '{timeleft}', '{size_str}', '{percentage}', '{status_text}', '{_escape_js(storage)}', {str(is_captcha).lower()})"
79
+ info_btn = f'<button class="btn-small info" onclick="{info_onclick}">ℹ️</button>'
80
+
81
+ # Action buttons - Info left, CAPTCHA/Delete right
72
82
  if is_captcha and nzo_id:
73
83
  actions = f'''
74
84
  <div class="package-actions">
75
- <button class="btn-small primary" onclick="location.href='/captcha?package_id={nzo_id}'">🔓 Solve CAPTCHA</button>
85
+ {info_btn}
86
+ <button class="btn-small primary-thin" onclick="location.href='/captcha?package_id={nzo_id}'">🔓 Solve CAPTCHA</button>
76
87
  <span class="spacer"></span>
77
88
  <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(display_name)}')">🗑️</button>
78
89
  </div>
79
90
  '''
80
91
  elif nzo_id:
81
92
  actions = f'''
82
- <div class="package-actions right-only">
93
+ <div class="package-actions">
94
+ {info_btn}
95
+ <span class="spacer"></span>
83
96
  <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(display_name)}')">🗑️</button>
84
97
  </div>
85
98
  '''
86
99
  else:
87
- actions = ''
100
+ actions = f'''
101
+ <div class="package-actions">
102
+ {info_btn}
103
+ <span class="spacer"></span>
104
+ </div>
105
+ '''
106
+
107
+ cat_html = f'<span title="Category: {cat}">{cat_emoji}</span>'
108
+ archive_html = f'<span title="Archive: {is_archive}">{archive_badge}</span>' if is_archive else ''
88
109
 
89
110
  return f'''
90
111
  <div class="package-card">
@@ -99,8 +120,8 @@ def _render_queue_item(item):
99
120
  <div class="package-details">
100
121
  <span>⏱️ {timeleft}</span>
101
122
  <span>💾 {size_str}</span>
102
- <span>{cat_emoji}</span>
103
- {archive_badge}
123
+ {cat_html}
124
+ {archive_html}
104
125
  </div>
105
126
  {actions}
106
127
  </div>
@@ -116,6 +137,7 @@ def _render_history_item(item):
116
137
  extraction_status = item.get('extraction_status', '')
117
138
  fail_message = item.get('fail_message', '')
118
139
  nzo_id = item.get('nzo_id', '')
140
+ storage = item.get('storage', '')
119
141
 
120
142
  is_error = status.lower() in ['failed', 'error'] or fail_message
121
143
  card_class = 'package-card error' if is_error else 'package-card'
@@ -123,27 +145,41 @@ def _render_history_item(item):
123
145
  cat_emoji = _get_category_emoji(category)
124
146
  size_str = _format_size(bytes_val=bytes_val)
125
147
 
126
- archive_badge = ''
148
+ archive_emoji = ''
127
149
  if is_archive:
128
150
  if extraction_status == 'SUCCESSFUL':
129
- archive_badge = '<span class="badge extracted">✅</span>'
151
+ archive_emoji = ''
130
152
  elif extraction_status == 'RUNNING':
131
- archive_badge = '<span class="badge pending">⏳</span>'
153
+ archive_emoji = ''
132
154
  else:
133
- archive_badge = '<span class="badge archive">📁</span>'
155
+ archive_emoji = '📦'
134
156
 
135
157
  status_emoji = '❌' if is_error else '✅'
136
158
  error_html = f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ''
137
159
 
160
+ # Interactive info
161
+ info_onclick = f"showPackageDetails('{nzo_id}', '{_escape_js(name)}', '{category}', '{'Yes' if is_archive else 'No'}', '{extraction_status}', '', '{size_str}', '', '{status}', '{_escape_js(storage)}', false)"
162
+ info_btn = f'<button class="btn-small info" onclick="{info_onclick}">ℹ️</button>'
163
+
138
164
  # Delete button for history items
139
165
  if nzo_id:
140
166
  actions = f'''
141
- <div class="package-actions right-only">
167
+ <div class="package-actions">
168
+ {info_btn}
169
+ <span class="spacer"></span>
142
170
  <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(name)}')">🗑️</button>
143
171
  </div>
144
172
  '''
145
173
  else:
146
- actions = ''
174
+ actions = f'''
175
+ <div class="package-actions">
176
+ {info_btn}
177
+ <span class="spacer"></span>
178
+ </div>
179
+ '''
180
+
181
+ cat_html = f'<span title="Category: {category}">{cat_emoji}</span>'
182
+ archive_html = f'<span title="Archive Status: {extraction_status}">{archive_emoji}</span>' if is_archive else ''
147
183
 
148
184
  return f'''
149
185
  <div class="{card_class}">
@@ -153,8 +189,8 @@ def _render_history_item(item):
153
189
  </div>
154
190
  <div class="package-details">
155
191
  <span>💾 {size_str}</span>
156
- <span>{cat_emoji}</span>
157
- {archive_badge}
192
+ {cat_html}
193
+ {archive_html}
158
194
  </div>
159
195
  {error_html}
160
196
  {actions}
@@ -261,7 +297,13 @@ def setup_packages_routes(app):
261
297
  device = None
262
298
 
263
299
  if not device:
264
- return '<p class="empty-message">JDownloader connection not established.</p>'
300
+ return '''
301
+ <div class="status-bar">
302
+ <span class="status-pill error">
303
+ ❌ JDownloader disconnected
304
+ </span>
305
+ </div>
306
+ '''
265
307
 
266
308
  return _render_packages_content()
267
309
 
@@ -278,7 +320,11 @@ def setup_packages_routes(app):
278
320
  back_btn = render_button("Back", "secondary", {"onclick": "location.href='/'"})
279
321
  return render_centered_html(f'''
280
322
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
281
- <p>JDownloader connection not established.</p>
323
+ <div class="status-bar">
324
+ <span class="status-pill error">
325
+ ❌ JDownloader disconnected
326
+ </span>
327
+ </div>
282
328
  <p>{back_btn}</p>
283
329
  ''')
284
330
 
@@ -328,12 +374,6 @@ def setup_packages_routes(app):
328
374
  .package-header {{ display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }}
329
375
  .status-emoji {{ font-size: 1.2em; flex-shrink: 0; }}
330
376
  .package-name {{ flex: 1; font-weight: 500; word-break: break-word; line-height: 1.3; }}
331
-
332
- .badge {{ font-size: 0.75em; padding: 2px 6px; border-radius: 4px; white-space: nowrap; flex-shrink: 0; }}
333
- .badge.archive {{ background: var(--badge-archive-bg, #e3f2fd); color: var(--badge-archive-color, #1565c0); }}
334
- .badge.extracted {{ background: var(--badge-success-bg, #e8f5e9); color: var(--badge-success-color, #2e7d32); }}
335
- .badge.pending {{ background: var(--badge-warning-bg, #fff3e0); color: var(--badge-warning-color, #e65100); }}
336
-
337
377
  .package-progress {{ display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }}
338
378
  .progress-track {{ flex: 1; height: 8px; background: var(--progress-track, #e0e0e0); border-radius: 4px; overflow: hidden; }}
339
379
  .progress-fill {{ height: 100%; background: var(--progress-fill, #4caf50); border-radius: 4px; min-width: 4px; }}
@@ -346,11 +386,15 @@ def setup_packages_routes(app):
346
386
  .package-actions {{ margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-color, #eee); display: flex; gap: 8px; align-items: center; }}
347
387
  .package-actions .spacer {{ flex: 1; }}
348
388
  .package-actions.right-only {{ justify-content: flex-end; }}
349
- .btn-small {{ padding: 5px 12px; font-size: 0.8em; border-radius: 4px; cursor: pointer; transition: all 0.2s; }}
389
+ .btn-small {{ line-height:1; padding: 5px 12px; font-size: 0.8em; border-radius: 4px; cursor: pointer; transition: all 0.2s; }}
350
390
  .btn-small.primary {{ background: var(--btn-primary-bg, #007bff); color: white; border: none; }}
351
391
  .btn-small.primary:hover {{ background: var(--btn-primary-hover, #0056b3); }}
352
392
  .btn-small.danger {{ background: transparent; color: var(--btn-danger-text, #dc3545); border: 1px solid var(--btn-danger-border, #dc3545); }}
353
393
  .btn-small.danger:hover {{ background: var(--btn-danger-hover-bg, #dc3545); color: white; }}
394
+ .btn-small.info {{ background: transparent; color: var(--btn-info-bg, #17a2b8); border: 1px solid var(--btn-info-bg, #17a2b8); }}
395
+ .btn-small.info:hover {{ background: var(--btn-info-bg, #17a2b8); color: white; }}
396
+ .btn-small.primary-thin {{ background: transparent; color: var(--btn-primary-bg, #007bff); border: 1px solid var(--btn-primary-bg, #007bff); }}
397
+ .btn-small.primary-thin:hover {{ background: var(--btn-primary-bg, #007bff); color: white; }}
354
398
 
355
399
  .empty-message {{ color: var(--text-muted, #888); font-style: italic; text-align: center; padding: 20px; }}
356
400
 
@@ -397,8 +441,8 @@ def setup_packages_routes(app):
397
441
  border: 1px solid var(--error-border, #f1aeb5);
398
442
  }}
399
443
 
400
- .btn-danger {{ background: var(--btn-danger-bg, #dc3545); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 500; }}
401
- .btn-danger:hover {{ opacity: 0.9; }}
444
+ .btn-danger {{ background: transparent; color: var(--btn-danger-bg, #dc3545); border: 1px solid var(--btn-danger-bg, #dc3545); padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 500; }}
445
+ .btn-danger:hover {{ background: var(--btn-danger-bg, #dc3545); color: white; }}
402
446
 
403
447
  /* Dark mode */
404
448
  @media (prefers-color-scheme: dark) {{
@@ -407,13 +451,11 @@ def setup_packages_routes(app):
407
451
  --border-color: #4a5568; --text-muted: #a0aec0;
408
452
  --progress-track: #4a5568; --progress-fill: #68d391;
409
453
  --error-border: #fc8181; --error-bg: #3d2d2d; --error-msg-bg: #3d2d2d; --error-msg-color: #fc8181;
410
- --badge-archive-bg: #1a365d; --badge-archive-color: #63b3ed;
411
- --badge-success-bg: #1c4532; --badge-success-color: #68d391;
412
- --badge-warning-bg: #3d2d1a; --badge-warning-color: #f6ad55;
413
454
  --link-color: #63b3ed; --modal-bg: #2d3748; --code-bg: #1a202c;
414
455
  --btn-primary-bg: #3182ce; --btn-primary-hover: #2c5282;
415
456
  --btn-danger-text: #fc8181; --btn-danger-border: #fc8181; --btn-danger-hover-bg: #e53e3e;
416
457
  --success-bg: #1c4532; --success-color: #68d391; --success-border: #276749;
458
+ --btn-info-bg: #38b2ac; --btn-info-hover: #319795;
417
459
  }}
418
460
  }}
419
461
  </style>
@@ -422,10 +464,14 @@ def setup_packages_routes(app):
422
464
  // Background refresh - fetches content via AJAX, waits 5s between refresh cycles
423
465
  let refreshPaused = false;
424
466
  let slowConnection = false;
467
+ let refreshTimer = null;
468
+ let isFetching = false;
425
469
 
426
470
  async function refreshContent() {{
427
471
  if (refreshPaused) return;
472
+ if (isFetching) return;
428
473
 
474
+ isFetching = true;
429
475
  const startTime = Date.now();
430
476
  const warningEl = document.getElementById('slow-warning');
431
477
 
@@ -465,8 +511,15 @@ def setup_packages_routes(app):
465
511
  }}
466
512
  }} catch (e) {{
467
513
  clearTimeout(slowTimer);
514
+ }} finally {{
515
+ isFetching = false;
516
+ }}
517
+
518
+ // Only schedule next refresh if not paused
519
+ if (!refreshPaused) {{
520
+ if (refreshTimer) clearTimeout(refreshTimer);
521
+ refreshTimer = setTimeout(refreshContent, 5000);
468
522
  }}
469
- setTimeout(refreshContent, 5000);
470
523
  }}
471
524
 
472
525
  function restoreCollapseState() {{
@@ -520,6 +573,10 @@ def setup_packages_routes(app):
520
573
  // Delete modal
521
574
  let deletePackageId = null;
522
575
  function confirmDelete(packageId, packageName) {{
576
+ // Stop any pending refresh
577
+ if (refreshTimer) clearTimeout(refreshTimer);
578
+ refreshPaused = true;
579
+
523
580
  deletePackageId = packageId;
524
581
 
525
582
  const content = `
@@ -530,12 +587,11 @@ def setup_packages_routes(app):
530
587
  `;
531
588
 
532
589
  const buttons = `
533
- <button class="btn-secondary" onclick="closeModal()">Cancel</button>
590
+ <button class="btn-secondary" onclick="closeModal()">Back</button>
534
591
  <button class="btn-danger" onclick="performDelete()">🗑️ Delete Package & Files</button>
535
592
  `;
536
593
 
537
594
  showModal('🗑️ Delete Package?', content, buttons);
538
- refreshPaused = true;
539
595
  }}
540
596
 
541
597
  function performDelete() {{
@@ -544,12 +600,53 @@ def setup_packages_routes(app):
544
600
  }}
545
601
  }}
546
602
 
603
+ // Show package details modal
604
+ function showPackageDetails(id, name, category, isArchive, extractionStatus, eta, size, percentage, status, storage, isCaptcha) {{
605
+ // Stop any pending refresh
606
+ if (refreshTimer) clearTimeout(refreshTimer);
607
+ refreshPaused = true;
608
+
609
+ let captchaBtn = '';
610
+ if (isCaptcha) {{
611
+ captchaBtn = `<button class="btn-small primary-thin" onclick="location.href='/captcha?package_id=${{id}}'">🔓 Solve CAPTCHA</button>`;
612
+ }}
613
+
614
+ const content = `
615
+ <div style="text-align: left; padding: 10px;">
616
+ <p style="margin-bottom: 8px;"><strong>Name:</strong></p><p style="font-family: monospace; text-align: center; background: var(--code-bg, #eee); padding: 4px; border-radius: 4px; word-break: break-word;">${{name}}</p>
617
+ ${{storage ? `<p style="margin-bottom: 4px;"><strong>Storage:</strong></p><p style="margin-bottom: 8px; font-family: monospace; text-align: center; background: var(--code-bg, #eee); padding: 4px; border-radius: 4px; word-break: break-all;">${{storage}}</p>` : ''}}
618
+ <p style="margin-bottom: 4px;"><strong>ID:</strong></p><p style="margin-bottom: 8px; font-family: monospace; text-align: center; background: var(--code-bg, #eee); padding: 4px; border-radius: 4px;">${{id}}</p>
619
+ <p style="margin-bottom: 8px;"><strong>Status:</strong> ${{status}}</p>
620
+ ${{percentage ? `<p style="margin-bottom: 8px;"><strong>Percentage:</strong> ${{percentage}}%</p>` : ''}}
621
+ ${{size ? `<p style="margin-bottom: 8px;"><strong>Size:</strong> ${{size}}</p>` : ''}}
622
+ ${{eta ? `<p style="margin-bottom: 8px;"><strong>ETA:</strong> ${{eta}}</p>` : ''}}
623
+ <p style="margin-bottom: 8px;"><strong>Category:</strong> ${{category}}</p>
624
+ <p style="margin-bottom: 8px;"><strong>Archive:</strong> ${{isArchive}}</p>
625
+ ${{extractionStatus ? `<p style="margin-bottom: 8px;"><strong>Extraction Status:</strong> ${{extractionStatus}}</p>` : ''}}
626
+ </div>
627
+ `;
628
+
629
+ const buttons = `
630
+ <button class="btn-secondary" onclick="closeModal()">Back</button>
631
+ ${{captchaBtn}}
632
+ `;
633
+
634
+ showModal('ℹ️ Package Details', content, buttons);
635
+ }}
636
+
547
637
  // Hook into modal closing to resume refresh
548
- const baseCloseModal = window.closeModal;
549
- window.closeModal = function() {{
550
- if (baseCloseModal) baseCloseModal();
551
- refreshPaused = false;
552
- }};
638
+ document.addEventListener('DOMContentLoaded', function() {{
639
+ const baseCloseModal = window.closeModal;
640
+ window.closeModal = function() {{
641
+ if (baseCloseModal) baseCloseModal();
642
+
643
+ // Clear any existing timer to prevent duplicates
644
+ if (refreshTimer) clearTimeout(refreshTimer);
645
+
646
+ refreshPaused = false;
647
+ refreshContent();
648
+ }};
649
+ }});
553
650
  </script>
554
651
  '''
555
652
 
@@ -72,6 +72,26 @@ def setup_statistics(app, shared_state):
72
72
  <div class="stat-value">{stats['failed_decryptions_manual']:,}</div>
73
73
  </div>
74
74
  </div>
75
+
76
+ <h3>🎬 IMDb Cache</h3>
77
+ <div class="stats-grid compact">
78
+ <div class="stat-card">
79
+ <h3>💾 Total Cached IDs</h3>
80
+ <div class="stat-value">{stats['imdb_total_cached']:,}</div>
81
+ </div>
82
+ <div class="stat-card">
83
+ <h3>🏷️ With Title</h3>
84
+ <div class="stat-value">{stats['imdb_with_title']:,}</div>
85
+ </div>
86
+ <div class="stat-card">
87
+ <h3>🖼️ With Poster</h3>
88
+ <div class="stat-value">{stats['imdb_with_poster']:,}</div>
89
+ </div>
90
+ <div class="stat-card">
91
+ <h3>🌍 With Localized Title</h3>
92
+ <div class="stat-value">{stats['imdb_with_localized']:,}</div>
93
+ </div>
94
+ </div>
75
95
  </div>
76
96
 
77
97
  <p>
@@ -463,10 +463,12 @@ def get_packages(shared_state, _cache=None):
463
463
 
464
464
  if package["location"] == "queue":
465
465
  time_left = "23:59:59"
466
+ storage = ""
466
467
 
467
468
  if package["type"] == "linkgrabber":
468
469
  details = package["details"]
469
470
  name = f"[Linkgrabber] {details.get('name', 'unknown')}"
471
+ storage = details.get("saveTo", "")
470
472
  try:
471
473
  bytes_total = int(details.get("bytesTotal", 0))
472
474
  mb = mb_left = bytes_total / (1024 * 1024)
@@ -481,6 +483,7 @@ def get_packages(shared_state, _cache=None):
481
483
  elif package["type"] == "downloader":
482
484
  details = package["details"]
483
485
  status = "Downloading"
486
+ storage = details.get("saveTo", "")
484
487
  pkg_eta = details.get("eta")
485
488
  bytes_total = int(details.get("bytesTotal", 0))
486
489
  bytes_loaded = int(details.get("bytesLoaded", 0))
@@ -536,7 +539,8 @@ def get_packages(shared_state, _cache=None):
536
539
  "timeleft": time_left,
537
540
  "type": package_type,
538
541
  "uuid": package_uuid,
539
- "is_archive": package.get("is_archive", False)
542
+ "is_archive": package.get("is_archive", False),
543
+ "storage": storage
540
544
  })
541
545
  queue_index += 1
542
546
  else:
@@ -577,7 +581,9 @@ def get_packages(shared_state, _cache=None):
577
581
  "type": "downloader",
578
582
  "uuid": package.get("uuid"),
579
583
  "is_archive": package.get("is_archive", False),
580
- "extraction_ok": package.get("extraction_ok", False)
584
+ "extraction_ok": package.get("extraction_ok", False),
585
+ "extraction_status": "SUCCESSFUL" if package.get("extraction_ok", False) else "RUNNING" if package.get(
586
+ "is_archive", False) else ""
581
587
  })
582
588
  history_index += 1
583
589
  else:
quasarr/providers/auth.py CHANGED
@@ -148,7 +148,7 @@ def _render_login_page(error=None):
148
148
  <meta charset="utf-8">
149
149
  <meta name="viewport" content="width=device-width, initial-scale=1">
150
150
  <title>Quasarr - Login</title>
151
- <link rel="icon" href="{images.logo}" type="image/png">
151
+ <link rel="icon" href="{images.favicon}" type="image/png">
152
152
  <style>
153
153
  :root {{
154
154
  --bg-color: #ffffff;
@@ -2,4 +2,5 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
- logo = 'data:image/webp;base64,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'
5
+ favicon = 'data:image/png;base64,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'
6
+ logo = 'data:image/webp;base64,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'
@@ -12,7 +12,7 @@ def render_centered_html(inner_content, footer_content=""):
12
12
  <meta charset="utf-8">
13
13
  <meta name="viewport" content="width=device-width, initial-scale=1">
14
14
  <title>Quasarr</title>
15
- <link rel="icon" href="''' + images.logo + '''" type="image/png">
15
+ <link rel="icon" href="''' + images.favicon + '''" type="image/png">
16
16
  <style>
17
17
  /* Theme variables */
18
18
  :root {
@@ -456,7 +456,7 @@ def get_imdb_metadata(imdb_id):
456
456
  if response_json:
457
457
  imdb_metadata["title"] = TitleCleaner.sanitize(response_json.get("primaryTitle", ""))
458
458
  imdb_metadata["year"] = response_json.get("startYear")
459
-
459
+
460
460
  days = 7 if imdb_metadata.get("title") and imdb_metadata.get("year") else 1
461
461
  imdb_metadata["ttl"] = now + timedelta(days=days).total_seconds()
462
462
 
@@ -32,7 +32,7 @@ def create_and_persist_session(shared_state):
32
32
  }
33
33
 
34
34
  r = dd_session.post(f'https://{dd}/index/index',
35
- cookies=cookies, headers=headers, data=data, timeout=10)
35
+ cookies=cookies, headers=headers, data=data, timeout=10)
36
36
  r.raise_for_status()
37
37
 
38
38
  error = False
@@ -2,6 +2,7 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
+ from json import loads
5
6
  from typing import Dict, Any
6
7
 
7
8
 
@@ -101,6 +102,51 @@ class StatsHelper:
101
102
  """Increment failed manual decryptions counter"""
102
103
  self._increment_stat("failed_decryptions_manual", 1)
103
104
 
105
+ def get_imdb_cache_stats(self) -> Dict[str, int]:
106
+ """
107
+ Get statistics about the IMDb metadata cache.
108
+ Returns counts of cached items with various attributes.
109
+ """
110
+ try:
111
+ db = self.shared_state.values["database"]("imdb_metadata")
112
+ all_entries = db.retrieve_all_titles()
113
+
114
+ total_cached = 0
115
+ with_title = 0
116
+ with_poster = 0
117
+ with_localized = 0
118
+
119
+ for _, data_str in all_entries:
120
+ try:
121
+ data = loads(data_str)
122
+ total_cached += 1
123
+
124
+ if data.get("title"):
125
+ with_title += 1
126
+
127
+ if data.get("poster_link"):
128
+ with_poster += 1
129
+
130
+ if data.get("localized") and isinstance(data["localized"], dict) and len(data["localized"]) > 0:
131
+ with_localized += 1
132
+
133
+ except (ValueError, TypeError):
134
+ continue
135
+
136
+ return {
137
+ "imdb_total_cached": total_cached,
138
+ "imdb_with_title": with_title,
139
+ "imdb_with_poster": with_poster,
140
+ "imdb_with_localized": with_localized
141
+ }
142
+ except Exception:
143
+ return {
144
+ "imdb_total_cached": 0,
145
+ "imdb_with_title": 0,
146
+ "imdb_with_poster": 0,
147
+ "imdb_with_localized": 0
148
+ }
149
+
104
150
  def get_stats(self) -> Dict[str, Any]:
105
151
  """Get all current statistics"""
106
152
  stats = {
@@ -151,4 +197,7 @@ class StatsHelper:
151
197
  )
152
198
  })
153
199
 
200
+ # Add IMDb cache stats
201
+ stats.update(self.get_imdb_cache_stats())
202
+
154
203
  return stats
@@ -3,12 +3,13 @@
3
3
  # Project by https://github.com/rix1337
4
4
 
5
5
  import re
6
+ import sys
6
7
 
7
- import requests
8
+ __version__ = "2.4.5"
8
9
 
9
10
 
10
11
  def get_version():
11
- return "2.4.1"
12
+ return __version__
12
13
 
13
14
 
14
15
  def get_latest_version():
@@ -17,6 +18,11 @@ def get_latest_version():
17
18
  Returns the tag name string (e.g. "1.5.0" or "1.4.2a1").
18
19
  Raises RuntimeError on HTTP errors.
19
20
  """
21
+ try:
22
+ import requests
23
+ except ImportError:
24
+ return __version__
25
+
20
26
  api_url = "https://api.github.com/repos/rix1337/Quasarr/releases/latest"
21
27
  resp = requests.get(api_url, headers={"Accept": "application/vnd.github.v3+json"})
22
28
  if resp.status_code != 200:
@@ -114,5 +120,7 @@ def create_version_file():
114
120
 
115
121
 
116
122
  if __name__ == '__main__':
117
- print(get_version())
118
- create_version_file()
123
+ if len(sys.argv) > 1 and sys.argv[1] == '--create-version-file':
124
+ create_version_file()
125
+ else:
126
+ print(get_version())
@@ -42,6 +42,7 @@ class Server:
42
42
  self.server.handle_request() # handle the last request
43
43
  except Exception:
44
44
  self.server.server_close()
45
+ temp_server_success = False
45
46
  return False
46
47
  time.sleep(1)
47
48
  self.server.server_close()
@@ -84,7 +84,7 @@ def he_search(shared_state, start_time, request_from, search_string="", mirror=N
84
84
  if not local_title:
85
85
  info(f"{hostname}: no title for IMDb {imdb_id}")
86
86
  return releases
87
- if not season:
87
+ if not season:
88
88
  year = get_year(imdb_id)
89
89
  if year:
90
90
  local_title += f" {year}"
@@ -97,7 +97,7 @@ def he_search(shared_state, start_time, request_from, search_string="", mirror=N
97
97
 
98
98
  if not source_search:
99
99
  search_type = "feed"
100
- timeout=30
100
+ timeout = 30
101
101
  else:
102
102
  search_type = "search"
103
103
  timeout = 10
quasarr/storage/setup.py CHANGED
@@ -154,6 +154,7 @@ def path_config(shared_state):
154
154
 
155
155
  info(f'Starting web server for config at: "{shared_state.values['internal_address']}".')
156
156
  info("Please set desired config path there!")
157
+ quasarr.providers.web_server.temp_server_success = False
157
158
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
158
159
 
159
160
 
@@ -161,7 +162,8 @@ def _escape_js_for_html_attr(s):
161
162
  """Escape a string for use inside a JS string literal within an HTML attribute."""
162
163
  if s is None:
163
164
  return ""
164
- return str(s).replace("\\", "\\\\").replace("'", "\\'").replace('"', '&quot;').replace("\n", "\\n").replace("\r", "")
165
+ return str(s).replace("\\", "\\\\").replace("'", "\\'").replace('"', '&quot;').replace("\n", "\\n").replace("\r",
166
+ "")
165
167
 
166
168
 
167
169
  def hostname_form_html(shared_state, message, show_restart_button=False, show_skip_management=False):
@@ -200,7 +202,7 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
200
202
  issue = hostname_issues.get(field_id)
201
203
  timestamp = ""
202
204
  operation = ""
203
- error_details_for_modal = "" # New variable to hold the full error message for the modal
205
+ error_details_for_modal = "" # New variable to hold the full error message for the modal
204
206
 
205
207
  if not current_value:
206
208
  status = "unset"
@@ -216,7 +218,7 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
216
218
  status = "error"
217
219
  status_emoji = "🔴"
218
220
  operation = issue.get("operation", "unknown")
219
- error_details_for_modal = issue.get("error", "Unknown error") # Get the full error message
221
+ error_details_for_modal = issue.get("error", "Unknown error") # Get the full error message
220
222
  timestamp = issue.get("timestamp", "")
221
223
  status_title = f"Error in {operation}"
222
224
  else:
@@ -290,7 +292,7 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
290
292
  margin-top: 0.5rem;
291
293
  font-size: 0.875rem;
292
294
  }}
293
- .import-status:empty {{
295
+ .import-status.empty {{
294
296
  display: none;
295
297
  }}
296
298
  .import-status.success {{ color: #198754; }}
@@ -367,14 +369,10 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
367
369
  function onHostnameFieldFocus() {{
368
370
  var urlInput = document.getElementById('hostnamesUrl');
369
371
  if (urlInput.value.trim() === '') {{
370
- var hasOpenedHelper = localStorage.getItem('hideHostnameHelperRedirect');
371
- if (!hasOpenedHelper) {{
372
- localStorage.setItem('hideHostnameHelperRedirect', 'true');
373
- window.open('https://quasarr-host.name', '_blank');
374
- var statusDiv = document.getElementById('importStatus');
375
- statusDiv.className = 'import-status';
376
- statusDiv.textContent = 'Opened hostname helper in new tab. Paste the URL here after setup.';
377
- }}
372
+ window.open('https://quasarr-host.name', '_blank');
373
+ var statusDiv = document.getElementById('importStatus');
374
+ statusDiv.className = 'import-status';
375
+ statusDiv.textContent = 'Opened hostname helper in new tab. Paste the URL here after setup.';
378
376
  }}
379
377
  }}
380
378
 
@@ -754,8 +752,26 @@ def hostnames_config(shared_state):
754
752
  except Exception as e:
755
753
  return {"success": False, "error": f"Error: {str(e)}"}
756
754
 
755
+ @app.get("/api/skip-login")
756
+ def get_skip_login():
757
+ """Return list of hostnames with skipped login."""
758
+ response.content_type = 'application/json'
759
+ skip_db = DataBase("skip_login")
760
+ login_required_sites = ['al', 'dd', 'dl', 'nx']
761
+ skipped = []
762
+ for site in login_required_sites:
763
+ if skip_db.retrieve(site):
764
+ skipped.append(site)
765
+ return {"skipped": skipped}
766
+
767
+ @app.delete('/api/skip-login/<shorthand>')
768
+ def clear_skip_login(shorthand):
769
+ DataBase("skip_login").delete(shorthand)
770
+ return {"success": True}
771
+
757
772
  info(f'Hostnames not set. Starting web server for config at: "{shared_state.values['internal_address']}".')
758
773
  info("Please set at least one valid hostname there!")
774
+ quasarr.providers.web_server.temp_server_success = False
759
775
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
760
776
 
761
777
 
@@ -876,6 +892,8 @@ def hostname_credentials_config(shared_state, shorthand, domain):
876
892
  password = request.forms.get('password')
877
893
  config = Config(shorthand)
878
894
 
895
+ error_message = "User and Password wrong or empty!"
896
+
879
897
  if user and password:
880
898
  config.save("user", user)
881
899
  config.save("password", password)
@@ -884,6 +902,10 @@ def hostname_credentials_config(shared_state, shorthand, domain):
884
902
  DataBase("skip_login").delete(sh.lower())
885
903
 
886
904
  if sh.lower() == "al":
905
+ error_message = ("User and Password wrong or empty.<br><br>"
906
+ "Or if you skipped Flaresolverr setup earlier, "
907
+ "you must chose to skip login for this site, "
908
+ "set up FlareSolverr in the UI and then restart Quasarr!")
887
909
  if quasarr.providers.sessions.al.create_and_persist_session(shared_state):
888
910
  quasarr.providers.web_server.temp_server_success = True
889
911
  return render_reconnect_success(f"{sh} credentials set successfully")
@@ -905,13 +927,14 @@ def hostname_credentials_config(shared_state, shorthand, domain):
905
927
 
906
928
  config.save("user", "")
907
929
  config.save("password", "")
908
- return render_fail("User and Password wrong or empty!")
930
+ return render_fail(error_message)
909
931
 
910
932
  info(
911
933
  f'"{shorthand.lower()}" credentials required to access download links. '
912
934
  f'Starting web server for config at: "{shared_state.values['internal_address']}".')
913
935
  info(f"If needed register here: 'https://{domain}'")
914
936
  info("Please set your credentials now, or skip to allow Quasarr to launch!")
937
+ quasarr.providers.web_server.temp_server_success = False
915
938
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
916
939
 
917
940
 
@@ -1053,6 +1076,7 @@ def flaresolverr_config(shared_state):
1053
1076
  f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
1054
1077
  )
1055
1078
  info("Please enter your FlareSolverr URL now, or skip to allow Quasarr to launch!")
1079
+ quasarr.providers.web_server.temp_server_success = False
1056
1080
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
1057
1081
 
1058
1082
 
@@ -1064,7 +1088,7 @@ def jdownloader_config(shared_state):
1064
1088
  @app.get('/')
1065
1089
  def jd_form():
1066
1090
  verify_form_html = f'''
1067
- <span>If required register account at: <a href="https://my.jdownloader.org/login.html#register">
1091
+ <span>If required register account at: <a href="https://my.jdownloader.org/login.html#register" target="_blank">
1068
1092
  my.jdownloader.org</a>!</span><br>
1069
1093
 
1070
1094
  <p><strong>JDownloader must be running and connected to My JDownloader!</strong></p><br>
@@ -1125,7 +1149,7 @@ def jdownloader_config(shared_state):
1125
1149
  document.getElementById("verifyButton").style.display = "none";
1126
1150
  document.getElementById('deviceForm').style.display = 'block';
1127
1151
  } else {
1128
- showModal('Error', 'Fehler! Bitte die Zugangsdaten überprüfen.');
1152
+ showModal('Error', 'Error! Please check your Credentials.');
1129
1153
  verifyInProgress = false;
1130
1154
  if (btn) { btn.disabled = false; btn.textContent = 'Verify Credentials'; }
1131
1155
  }
@@ -1188,4 +1212,5 @@ def jdownloader_config(shared_state):
1188
1212
  f'Starting web server for config at: "{shared_state.values['internal_address']}".')
1189
1213
  info("If needed register here: 'https://my.jdownloader.org/login.html#register'")
1190
1214
  info("Please set your credentials now, to allow Quasarr to launch!")
1215
+ quasarr.providers.web_server.temp_server_success = False
1191
1216
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
@@ -1,29 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quasarr
3
- Version: 2.4.1
3
+ Version: 2.4.5
4
4
  Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
5
- Home-page: https://github.com/rix1337/Quasarr
6
- Author: rix1337
7
- Author-email:
8
- Classifier: Programming Language :: Python :: 3
5
+ Author-email: rix1337 <rix1337@users.noreply.github.com>
6
+ License-File: LICENSE
9
7
  Classifier: License :: OSI Approved :: MIT License
10
8
  Classifier: Operating System :: OS Independent
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.12
13
11
  Requires-Dist: beautifulsoup4>=4.14.3
14
12
  Requires-Dist: bottle>=0.13.4
15
13
  Requires-Dist: dukpy>=0.5.0
16
14
  Requires-Dist: pillow>=12.0.0
17
15
  Requires-Dist: pycryptodomex>=3.23.0
18
16
  Requires-Dist: requests>=2.32.5
19
- Dynamic: author
20
- Dynamic: classifier
21
- Dynamic: description
22
- Dynamic: description-content-type
23
- Dynamic: home-page
24
- Dynamic: license-file
25
- Dynamic: requires-dist
26
- Dynamic: summary
17
+ Description-Content-Type: text/markdown
27
18
 
28
19
  #
29
20
 
@@ -206,8 +197,7 @@ docker run -d \
206
197
  | `EXTERNAL_ADDRESS` | Optional. External URL (e.g. reverse proxy). Always protect external access with authentication. |
207
198
  | `DISCORD` | Optional. Discord webhook URL for notifications. |
208
199
  | `HOSTNAMES` | Optional. URL to a hostname list to skip manual setup. Must be a publicly accessible `HTTP`/`HTTPS` link, point to a raw `.ini` or plain text file (not HTML or JSON), and contain at least one hostname per line in the format `ab = xyz`. |
209
- | `USER` | Username to protect the web UI. |
210
- | `PASS` | Password to protect the web UI. |
200
+ | `USER` / `PASS` | Optional, but recommended! Username / Password to protect the web UI. |
211
201
  | `AUTH` | Authentication mode. Supported values: `form` or `basic`. |
212
202
  | `SILENT` | Optional. If `True`, silences all Discord notifications except SponsorHelper error messages. |
213
203
  | `DEBUG` | Optional. If `True`, enables debug logging. |
@@ -215,13 +205,14 @@ docker run -d \
215
205
 
216
206
  # Manual setup
217
207
 
218
- Use this only in case you can't run the docker image.
208
+ > Use this only in case you can't run the docker image.
219
209
 
220
- `pip install quasarr`
210
+ > ⚠️ Requires Python 3.12 (or later) and [uv](https://docs.astral.sh/uv/#installation)!
221
211
 
222
- * Requires Python 3.12 or later
212
+ `uv tool install quasarr`
223
213
 
224
214
  ```
215
+ quasarr
225
216
  --port=8080
226
217
  --discord=https://discord.com/api/webhooks/1234567890/ABCDEFGHIJKLMN
227
218
  --external_address=https://foo.bar/
@@ -1,17 +1,17 @@
1
- quasarr/__init__.py,sha256=QRu_dlfLdToYkeO96bHaA8Kp0GmSL0IZc9ceRSxKWS0,15766
2
- quasarr/api/__init__.py,sha256=KLnFSe5l3MrVgrbu6-7GlE2PqouVyizqiRZfQkBtge0,19587
1
+ quasarr/__init__.py,sha256=CU4S8Kb1RpzePtFoS2tpOj1S6wDaTZeg7TmkmwejA24,16108
2
+ quasarr/api/__init__.py,sha256=kFePULH7xR--gAD73DS1J9V4q5s3On5da4NzupHgkaY,19743
3
3
  quasarr/api/arr/__init__.py,sha256=eEop8A5t936uT5azn4qz0bq1DMX84_Ja16wyleGFhyM,18495
4
- quasarr/api/captcha/__init__.py,sha256=JLHWssqy8AOu0cRaqEW9EH2X03FCzvE6VwZsB9YAnqE,73592
4
+ quasarr/api/captcha/__init__.py,sha256=GDuhmoAkwEqZdlpnf7LP_TdsBtPaV4MKnvJueo5ARaQ,73705
5
5
  quasarr/api/config/__init__.py,sha256=q-7vK5YULrSDgTicho--bNK8aAhcbzCdhhNwEwUEwWg,14173
6
- quasarr/api/packages/__init__.py,sha256=ox0vzuXByag49RUEwYPWtMacsXl_iksvubHgDmG5RWQ,25192
6
+ quasarr/api/packages/__init__.py,sha256=PrJbXxw0bqW_8u-MLFL4Uje_lvwZClgBWI2iqumZLy8,30719
7
7
  quasarr/api/sponsors_helper/__init__.py,sha256=PAXjlNWRf8Cf2s7CFLPOyHUjLPkLvI7LC-m97Q8tGh0,6215
8
- quasarr/api/statistics/__init__.py,sha256=0Os2rbqQ8ZN3R0XAavGVHlacKsAjp7GYjEIJCwvnsl8,7063
8
+ quasarr/api/statistics/__init__.py,sha256=QB1R0IK9DIhr6zHAd1Kk__2h0NdEgB30ECphDRqCf_g,7943
9
9
  quasarr/downloads/__init__.py,sha256=ikoHK5C8veDiU4M3eoDaUjFl0pYPSa91_7h65qEFiUM,16435
10
10
  quasarr/downloads/linkcrypters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  quasarr/downloads/linkcrypters/al.py,sha256=mfUG5VclC_-FcGoZL9zHYD7dz7X_YpaNmoKkgiyl9-0,8812
12
12
  quasarr/downloads/linkcrypters/filecrypt.py,sha256=nUZCTmvKylaNk1KAXcYUV1FgQCVAKNE3roXCNaqHLYA,17057
13
13
  quasarr/downloads/linkcrypters/hide.py,sha256=H4hJWhENkszV1u_ULC3aOW2fu9infC-Nv-7wx2DYqrA,6266
14
- quasarr/downloads/packages/__init__.py,sha256=VUbDnJqhqNDMLwCHYyWQKFKhU32xGMPXdrO0N5fxH8Q,32747
14
+ quasarr/downloads/packages/__init__.py,sha256=MvMqlpmo6ruFNfnfp8dlS-Mzf1AKTai1qP65SlY-YDw,33086
15
15
  quasarr/downloads/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  quasarr/downloads/sources/al.py,sha256=cV0rOpda4YHmv5X07855wzoaczd3yFGdPyFdm07srFM,27842
17
17
  quasarr/downloads/sources/by.py,sha256=ThyEVblYxaxKS_iROpxLhpqp2gOpcHjI1TCKX7CtrNw,4310
@@ -30,25 +30,25 @@ quasarr/downloads/sources/sl.py,sha256=zhU3C172IwyfrVmFSwZ34PywERHJFk0ownT2gbpUA
30
30
  quasarr/downloads/sources/wd.py,sha256=pZrLnRvVFzsZTWTIcHLCnUlSz5Y6HmEJpqcZjDigx2E,4820
31
31
  quasarr/downloads/sources/wx.py,sha256=jk_RSKKPa8iDPXfXKERG_4NwRMkFV6_rJhJ-zZlNQuo,6855
32
32
  quasarr/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- quasarr/providers/auth.py,sha256=ELJeKtJL0zy1UuBORzQibN6frAnh3lMv_HiofSW0hXA,10533
33
+ quasarr/providers/auth.py,sha256=ve89PgDGmDCRvzE8XS29gcuGO2KgTdK0aX5627prnYI,10536
34
34
  quasarr/providers/cloudflare.py,sha256=oUDR7OQ8E-8vCtagZLnIS2ZZV3ERffhxmW0njKKbtf0,7867
35
35
  quasarr/providers/hostname_issues.py,sha256=9PJFIosLB-bMTmgWlR5-sYAmcyps7TDoSYjoL9cw9TE,1460
36
- quasarr/providers/html_images.py,sha256=rrovPNl-FTTKKA-4HCPEhsYpq5b20VDrsB7t4RrQf3w,15531
37
- quasarr/providers/html_templates.py,sha256=IGWwt78bP2oJx4VzOP6w9zp7KVXgDY6Qz5ySL9cLGWI,15815
38
- quasarr/providers/imdb_metadata.py,sha256=a_kn9lw5cj5ZbxtrRBQKyF78ctMgHJJTW0DF2DONWOY,20771
36
+ quasarr/providers/html_images.py,sha256=G3tttyGbsV-IR8qIS9NUMX2J0-onxNUplcon70-YDWQ,19755
37
+ quasarr/providers/html_templates.py,sha256=H0Zd_hxmL49pXcEgkERqnxb1fO2Zd6boEsCsoypEZoM,15818
38
+ quasarr/providers/imdb_metadata.py,sha256=nF2UBiCR7zdGEnnQFrR1yw6q0w5iM5JawJCzCKEOmZw,20763
39
39
  quasarr/providers/jd_cache.py,sha256=mSvMrs3UwTn3sd9yGSJKGT-qwYeyYKC_l8whpXTVn7s,13530
40
40
  quasarr/providers/log.py,sha256=_g5RwtfuksARXnvryhsngzoJyFcNzj6suqd3ndqZM0Y,313
41
41
  quasarr/providers/myjd_api.py,sha256=Z3PEiO3c3UfDSr4Up5rgwTAnjloWHb-H1RkJ6BLKZv8,34140
42
42
  quasarr/providers/notifications.py,sha256=hMMAKKXTi_MCEJKaTWdTq7cGkY4z-wIFx_x1sH5P6hA,4811
43
43
  quasarr/providers/obfuscated.py,sha256=EYm_7SfdJd9ae_m4HZgY9ruDXC5J9hb4KEV_WAnk-ms,2275588
44
44
  quasarr/providers/shared_state.py,sha256=5a_ZbGqTvt4-OqBt2a1WtR9I5J_Ky7IlkEY8EGtKVu8,30646
45
- quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
45
+ quasarr/providers/statistics.py,sha256=ne8O3hLmIAsLvoOTczijqSV0uirUTS2KgcfYIsOXshQ,8021
46
46
  quasarr/providers/utils.py,sha256=mcUPbcXMsLmrYv0CTZO5a9aOt2-JLyL3SZxu6N8OyjU,12075
47
- quasarr/providers/version.py,sha256=QaiXWdeAfaBwMUbdR9c046Vj8QKA7e1LuUiZkY2Q5nI,4003
48
- quasarr/providers/web_server.py,sha256=AYd0KRxdDWMBr87BP8wlSMuL4zZo0I_rY-vHBai6Pfg,1688
47
+ quasarr/providers/version.py,sha256=kYwfXee03t6k0PenLlLzUS0iTFcYdOlbDO2c_n6dJbg,4196
48
+ quasarr/providers/web_server.py,sha256=M1dGcEg_VuYpjcpGIrJS4WryCLiPQrCdtTUOlc9zlAI,1728
49
49
  quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  quasarr/providers/sessions/al.py,sha256=AQ59vVU7uQSuwZLNppNsZAFvpow3zcxQ29dirPbyYc4,13432
51
- quasarr/providers/sessions/dd.py,sha256=ty9dnDFVJs-tFNcTS5QT9_wP82cKQGnCvb6v5In3Mog,3324
51
+ quasarr/providers/sessions/dd.py,sha256=d6tYYtEgXWD5AeIBjcdzQnlay24Q5lT2kt_1fSV1KdE,3314
52
52
  quasarr/providers/sessions/dl.py,sha256=yTJlD84ItotViA1d-m0RwrbEJlL-VK-0nGw_4kfNLe0,5923
53
53
  quasarr/providers/sessions/nx.py,sha256=ZuWuqfb_rPJVom0c1dsXefXPXdzAIYqnQZapOPaUYUI,3421
54
54
  quasarr/search/__init__.py,sha256=1Z4dEfbbTiUSs139S5_5VivnXszQjQrLn9AQPX87OyU,5920
@@ -61,7 +61,7 @@ quasarr/search/sources/dl.py,sha256=L4GK58Mp46dAZzmwtMB4ia1w0SSpp3z3eFvrmT-5278,
61
61
  quasarr/search/sources/dt.py,sha256=hvOqPKQRw5joSaTb9mpdPZXL4xpU167SFmLg8yhsPwM,10227
62
62
  quasarr/search/sources/dw.py,sha256=hna1ueKjdi9uqRQJ7UPenT0ym7igQgWGrv_--yGChVs,8215
63
63
  quasarr/search/sources/fx.py,sha256=xZUrv7dJSSmeLR2xnRQsRZAk9Q0-fDfQLNjz4wdBTqo,9452
64
- quasarr/search/sources/he.py,sha256=LZM5JquDdocTpqRUS7ObYEwEGo5pyJWOvZ91GCp7YJ8,7378
64
+ quasarr/search/sources/he.py,sha256=BQhDlaIrPR6GMoQpOZNNOAOOA_ZAMIYa6GQsvuxd_M8,7379
65
65
  quasarr/search/sources/mb.py,sha256=Hq1zupo27FzYSQUio03HPG0wP4jYwOXl6cqgdOpjlzQ,8178
66
66
  quasarr/search/sources/nk.py,sha256=Y-FgWmKyiPqcTdDsAGviClL_wyip7zPDNwrSPCcx4Ew,7146
67
67
  quasarr/search/sources/nx.py,sha256=UXUSYEL4zwYVwCri359I26GYN8CDuCKokpOOR21YEns,7602
@@ -72,11 +72,10 @@ quasarr/search/sources/wd.py,sha256=IvB0Lm8Vb0XI-OwuqCccakhMSQBoTKhEDnvrKDTmP14,
72
72
  quasarr/search/sources/wx.py,sha256=E7vSLV11540pson1HU-WVb7v7oX67kW8Y3-wiS-mv7w,13844
73
73
  quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
74
  quasarr/storage/config.py,sha256=SSTgIce2FVYoVTK_6OCU3msknhxuLA3EC4Kcrrf_dxQ,6378
75
- quasarr/storage/setup.py,sha256=Cbo0phZbC6JP2wx_qER3vpaLSTDLbKEfdXj6KoAMkWw,47403
75
+ quasarr/storage/setup.py,sha256=Vy8YEL85KrNZdrXIuXKUj2hLBNn_JQ9onC6oP06Xq1g,48613
76
76
  quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
77
- quasarr-2.4.1.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
78
- quasarr-2.4.1.dist-info/METADATA,sha256=ydtvVb2HIGnkEI6YzNMuoaJfiYrqxwXBzywdjw18Lns,15024
79
- quasarr-2.4.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
80
- quasarr-2.4.1.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
81
- quasarr-2.4.1.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
82
- quasarr-2.4.1.dist-info/RECORD,,
77
+ quasarr-2.4.5.dist-info/METADATA,sha256=_asH0B1lSZlhbsicu8zCH8vskDa2kFOz4RDFYHaknxU,14669
78
+ quasarr-2.4.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
79
+ quasarr-2.4.5.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
80
+ quasarr-2.4.5.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
81
+ quasarr-2.4.5.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1 +0,0 @@
1
- quasarr