quasarr 2.4.8__py3-none-any.whl → 2.4.10__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.

Files changed (76) hide show
  1. quasarr/__init__.py +134 -70
  2. quasarr/api/__init__.py +40 -31
  3. quasarr/api/arr/__init__.py +116 -108
  4. quasarr/api/captcha/__init__.py +262 -137
  5. quasarr/api/config/__init__.py +76 -46
  6. quasarr/api/packages/__init__.py +138 -102
  7. quasarr/api/sponsors_helper/__init__.py +29 -16
  8. quasarr/api/statistics/__init__.py +19 -19
  9. quasarr/downloads/__init__.py +165 -72
  10. quasarr/downloads/linkcrypters/al.py +35 -18
  11. quasarr/downloads/linkcrypters/filecrypt.py +107 -52
  12. quasarr/downloads/linkcrypters/hide.py +5 -6
  13. quasarr/downloads/packages/__init__.py +342 -177
  14. quasarr/downloads/sources/al.py +191 -100
  15. quasarr/downloads/sources/by.py +31 -13
  16. quasarr/downloads/sources/dd.py +27 -14
  17. quasarr/downloads/sources/dj.py +1 -3
  18. quasarr/downloads/sources/dl.py +126 -71
  19. quasarr/downloads/sources/dt.py +11 -5
  20. quasarr/downloads/sources/dw.py +28 -14
  21. quasarr/downloads/sources/he.py +32 -24
  22. quasarr/downloads/sources/mb.py +19 -9
  23. quasarr/downloads/sources/nk.py +14 -10
  24. quasarr/downloads/sources/nx.py +8 -18
  25. quasarr/downloads/sources/sf.py +45 -20
  26. quasarr/downloads/sources/sj.py +1 -3
  27. quasarr/downloads/sources/sl.py +9 -5
  28. quasarr/downloads/sources/wd.py +32 -12
  29. quasarr/downloads/sources/wx.py +35 -21
  30. quasarr/providers/auth.py +42 -37
  31. quasarr/providers/cloudflare.py +28 -30
  32. quasarr/providers/hostname_issues.py +2 -1
  33. quasarr/providers/html_images.py +2 -2
  34. quasarr/providers/html_templates.py +22 -14
  35. quasarr/providers/imdb_metadata.py +149 -80
  36. quasarr/providers/jd_cache.py +131 -39
  37. quasarr/providers/log.py +1 -1
  38. quasarr/providers/myjd_api.py +260 -196
  39. quasarr/providers/notifications.py +53 -41
  40. quasarr/providers/obfuscated.py +9 -4
  41. quasarr/providers/sessions/al.py +71 -55
  42. quasarr/providers/sessions/dd.py +21 -14
  43. quasarr/providers/sessions/dl.py +30 -19
  44. quasarr/providers/sessions/nx.py +23 -14
  45. quasarr/providers/shared_state.py +292 -141
  46. quasarr/providers/statistics.py +75 -43
  47. quasarr/providers/utils.py +33 -27
  48. quasarr/providers/version.py +45 -14
  49. quasarr/providers/web_server.py +10 -5
  50. quasarr/search/__init__.py +30 -18
  51. quasarr/search/sources/al.py +124 -73
  52. quasarr/search/sources/by.py +110 -59
  53. quasarr/search/sources/dd.py +57 -35
  54. quasarr/search/sources/dj.py +69 -48
  55. quasarr/search/sources/dl.py +159 -100
  56. quasarr/search/sources/dt.py +110 -74
  57. quasarr/search/sources/dw.py +121 -61
  58. quasarr/search/sources/fx.py +108 -62
  59. quasarr/search/sources/he.py +78 -49
  60. quasarr/search/sources/mb.py +96 -48
  61. quasarr/search/sources/nk.py +80 -50
  62. quasarr/search/sources/nx.py +91 -62
  63. quasarr/search/sources/sf.py +171 -106
  64. quasarr/search/sources/sj.py +69 -48
  65. quasarr/search/sources/sl.py +115 -71
  66. quasarr/search/sources/wd.py +67 -44
  67. quasarr/search/sources/wx.py +188 -123
  68. quasarr/storage/config.py +65 -52
  69. quasarr/storage/setup.py +238 -140
  70. quasarr/storage/sqlite_database.py +10 -4
  71. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/METADATA +4 -3
  72. quasarr-2.4.10.dist-info/RECORD +81 -0
  73. quasarr-2.4.8.dist-info/RECORD +0 -81
  74. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/WHEEL +0 -0
  75. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/entry_points.txt +0 -0
  76. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/licenses/LICENSE +0 -0
@@ -3,13 +3,15 @@
3
3
  # Project by https://github.com/rix1337
4
4
 
5
5
  import quasarr.providers.html_images as images
6
- from quasarr.downloads.packages import get_packages, delete_package
6
+ from quasarr.downloads.packages import delete_package, get_packages
7
7
  from quasarr.providers import shared_state
8
8
  from quasarr.providers.html_templates import render_button, render_centered_html
9
9
 
10
10
 
11
11
  def _get_category_emoji(cat):
12
- return {'movies': '🎬', 'tv': '📺', 'docs': '📄', 'not_quasarr': ''}.get(cat, '❓')
12
+ return {"movies": "🎬", "tv": "📺", "docs": "📄", "not_quasarr": ""}.get(
13
+ cat, "❓"
14
+ )
13
15
 
14
16
 
15
17
  def _format_size(mb=None, bytes_val=None):
@@ -29,42 +31,53 @@ def _format_size(mb=None, bytes_val=None):
29
31
 
30
32
 
31
33
  def _escape_js(s):
32
- return s.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
34
+ return (
35
+ s.replace("\\", "\\\\")
36
+ .replace("'", "\\'")
37
+ .replace('"', '\\"')
38
+ .replace("\n", "\\n")
39
+ )
33
40
 
34
41
 
35
42
  def _render_queue_item(item):
36
- filename = item.get('filename', 'Unknown')
37
- percentage = item.get('percentage', 0)
38
- timeleft = item.get('timeleft', '??:??:??')
39
- bytes_val = item.get('bytes', 0)
40
- mb = item.get('mb', 0)
41
- cat = item.get('cat', 'not_quasarr')
42
- is_archive = item.get('is_archive', False)
43
- nzo_id = item.get('nzo_id', '')
44
- storage = item.get('storage', '')
45
-
46
- is_captcha = '[CAPTCHA' in filename
47
- status_text = 'Downloading'
43
+ filename = item.get("filename", "Unknown")
44
+ percentage = item.get("percentage", 0)
45
+ timeleft = item.get("timeleft", "??:??:??")
46
+ bytes_val = item.get("bytes", 0)
47
+ mb = item.get("mb", 0)
48
+ cat = item.get("cat", "not_quasarr")
49
+ is_archive = item.get("is_archive", False)
50
+ nzo_id = item.get("nzo_id", "")
51
+ storage = item.get("storage", "")
52
+
53
+ is_captcha = "[CAPTCHA" in filename
54
+ status_text = "Downloading"
48
55
  if is_captcha:
49
- status_emoji = '🔒'
50
- status_text = 'Waiting for CAPTCHA Solution!'
51
- elif '[Extracting]' in filename:
52
- status_emoji = '📦'
53
- status_text = 'Extracting'
54
- elif '[Paused]' in filename:
55
- status_emoji = '⏸️'
56
- status_text = 'Paused'
57
- elif '[Linkgrabber]' in filename:
58
- status_emoji = '🔗'
59
- status_text = 'Linkgrabber'
56
+ status_emoji = "🔒"
57
+ status_text = "Waiting for CAPTCHA Solution!"
58
+ elif "[Extracting]" in filename:
59
+ status_emoji = "📦"
60
+ status_text = "Extracting"
61
+ elif "[Paused]" in filename:
62
+ status_emoji = "⏸️"
63
+ status_text = "Paused"
64
+ elif "[Linkgrabber]" in filename:
65
+ status_emoji = "🔗"
66
+ status_text = "Linkgrabber"
60
67
  else:
61
- status_emoji = '▶️'
68
+ status_emoji = "▶️"
62
69
 
63
70
  display_name = filename
64
- for prefix in ['[Downloading] ', '[Extracting] ', '[Paused] ', '[Linkgrabber] ', '[CAPTCHA not solved!] ']:
65
- display_name = display_name.replace(prefix, '')
66
-
67
- archive_badge = '📦' if is_archive else ''
71
+ for prefix in [
72
+ "[Downloading] ",
73
+ "[Extracting] ",
74
+ "[Paused] ",
75
+ "[Linkgrabber] ",
76
+ "[CAPTCHA not solved!] ",
77
+ ]:
78
+ display_name = display_name.replace(prefix, "")
79
+
80
+ archive_badge = "📦" if is_archive else ""
68
81
  cat_emoji = _get_category_emoji(cat)
69
82
  size_str = _format_size(bytes_val=bytes_val) if bytes_val else _format_size(mb=mb)
70
83
 
@@ -80,34 +93,38 @@ def _render_queue_item(item):
80
93
 
81
94
  # Action buttons - Info left, CAPTCHA/Delete right
82
95
  if is_captcha and nzo_id:
83
- actions = f'''
96
+ actions = f"""
84
97
  <div class="package-actions">
85
98
  {info_btn}
86
99
  <button class="btn-small primary-thin" onclick="location.href='/captcha?package_id={nzo_id}'">🔓 Solve CAPTCHA</button>
87
100
  <span class="spacer"></span>
88
101
  <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(display_name)}')">🗑️</button>
89
102
  </div>
90
- '''
103
+ """
91
104
  elif nzo_id:
92
- actions = f'''
105
+ actions = f"""
93
106
  <div class="package-actions">
94
107
  {info_btn}
95
108
  <span class="spacer"></span>
96
109
  <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(display_name)}')">🗑️</button>
97
110
  </div>
98
- '''
111
+ """
99
112
  else:
100
- actions = f'''
113
+ actions = f"""
101
114
  <div class="package-actions">
102
115
  {info_btn}
103
116
  <span class="spacer"></span>
104
117
  </div>
105
- '''
118
+ """
106
119
 
107
120
  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 ''
121
+ archive_html = (
122
+ f'<span title="Archive: {is_archive}">{archive_badge}</span>'
123
+ if is_archive
124
+ else ""
125
+ )
109
126
 
110
- return f'''
127
+ return f"""
111
128
  <div class="package-card">
112
129
  <div class="package-header">
113
130
  <span class="status-emoji">{status_emoji}</span>
@@ -125,37 +142,39 @@ def _render_queue_item(item):
125
142
  </div>
126
143
  {actions}
127
144
  </div>
128
- '''
145
+ """
129
146
 
130
147
 
131
148
  def _render_history_item(item):
132
- name = item.get('name', 'Unknown')
133
- status = item.get('status', 'Unknown')
134
- bytes_val = item.get('bytes', 0)
135
- category = item.get('category', 'not_quasarr')
136
- is_archive = item.get('is_archive', False)
137
- extraction_status = item.get('extraction_status', '')
138
- fail_message = item.get('fail_message', '')
139
- nzo_id = item.get('nzo_id', '')
140
- storage = item.get('storage', '')
141
-
142
- is_error = status.lower() in ['failed', 'error'] or fail_message
143
- card_class = 'package-card error' if is_error else 'package-card'
149
+ name = item.get("name", "Unknown")
150
+ status = item.get("status", "Unknown")
151
+ bytes_val = item.get("bytes", 0)
152
+ category = item.get("category", "not_quasarr")
153
+ is_archive = item.get("is_archive", False)
154
+ extraction_status = item.get("extraction_status", "")
155
+ fail_message = item.get("fail_message", "")
156
+ nzo_id = item.get("nzo_id", "")
157
+ storage = item.get("storage", "")
158
+
159
+ is_error = status.lower() in ["failed", "error"] or fail_message
160
+ card_class = "package-card error" if is_error else "package-card"
144
161
 
145
162
  cat_emoji = _get_category_emoji(category)
146
163
  size_str = _format_size(bytes_val=bytes_val)
147
164
 
148
- archive_emoji = ''
165
+ archive_emoji = ""
149
166
  if is_archive:
150
- if extraction_status == 'SUCCESSFUL':
151
- archive_emoji = ''
152
- elif extraction_status == 'RUNNING':
153
- archive_emoji = ''
167
+ if extraction_status == "SUCCESSFUL":
168
+ archive_emoji = ""
169
+ elif extraction_status == "RUNNING":
170
+ archive_emoji = ""
154
171
  else:
155
- archive_emoji = '📦'
172
+ archive_emoji = "📦"
156
173
 
157
- status_emoji = '' if is_error else ''
158
- error_html = f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ''
174
+ status_emoji = "" if is_error else ""
175
+ error_html = (
176
+ f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ""
177
+ )
159
178
 
160
179
  # Interactive info
161
180
  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)"
@@ -163,23 +182,27 @@ def _render_history_item(item):
163
182
 
164
183
  # Delete button for history items
165
184
  if nzo_id:
166
- actions = f'''
185
+ actions = f"""
167
186
  <div class="package-actions">
168
187
  {info_btn}
169
188
  <span class="spacer"></span>
170
189
  <button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(name)}')">🗑️</button>
171
190
  </div>
172
- '''
191
+ """
173
192
  else:
174
- actions = f'''
193
+ actions = f"""
175
194
  <div class="package-actions">
176
195
  {info_btn}
177
196
  <span class="spacer"></span>
178
197
  </div>
179
- '''
198
+ """
180
199
 
181
200
  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 ''
201
+ archive_html = (
202
+ f'<span title="Archive Status: {extraction_status}">{archive_emoji}</span>'
203
+ if is_archive
204
+ else ""
205
+ )
183
206
 
184
207
  return f'''
185
208
  <div class="{card_class}">
@@ -201,14 +224,14 @@ def _render_history_item(item):
201
224
  def _render_packages_content():
202
225
  """Render just the packages content (used for both full page and AJAX refresh)."""
203
226
  downloads = get_packages(shared_state)
204
- queue = downloads.get('queue', [])
205
- history = downloads.get('history', [])
227
+ queue = downloads.get("queue", [])
228
+ history = downloads.get("history", [])
206
229
 
207
230
  # Separate Quasarr packages from others
208
- quasarr_queue = [p for p in queue if p.get('cat') != 'not_quasarr']
209
- other_queue = [p for p in queue if p.get('cat') == 'not_quasarr']
210
- quasarr_history = [p for p in history if p.get('category') != 'not_quasarr']
211
- other_history = [p for p in history if p.get('category') == 'not_quasarr']
231
+ quasarr_queue = [p for p in queue if p.get("cat") != "not_quasarr"]
232
+ other_queue = [p for p in queue if p.get("cat") == "not_quasarr"]
233
+ quasarr_history = [p for p in history if p.get("category") != "not_quasarr"]
234
+ other_history = [p for p in history if p.get("category") == "not_quasarr"]
212
235
 
213
236
  # Check if there's anything at all
214
237
  has_quasarr_content = quasarr_queue or quasarr_history
@@ -216,42 +239,50 @@ def _render_packages_content():
216
239
  has_any_content = has_quasarr_content or has_other_content
217
240
 
218
241
  # Build queue section (only if has items)
219
- queue_html = ''
242
+ queue_html = ""
220
243
  if quasarr_queue:
221
- queue_items = ''.join(_render_queue_item(item) for item in quasarr_queue)
222
- queue_html = f'''
244
+ queue_items = "".join(_render_queue_item(item) for item in quasarr_queue)
245
+ queue_html = f"""
223
246
  <div class="section">
224
247
  <h3>⬇️ Downloading</h3>
225
248
  <div class="packages-list">{queue_items}</div>
226
249
  </div>
227
- '''
250
+ """
228
251
 
229
252
  # Build history section (only if has items)
230
- history_html = ''
253
+ history_html = ""
231
254
  if quasarr_history:
232
- history_items = ''.join(_render_history_item(item) for item in quasarr_history[:10])
233
- history_html = f'''
255
+ history_items = "".join(
256
+ _render_history_item(item) for item in quasarr_history[:10]
257
+ )
258
+ history_html = f"""
234
259
  <div class="section">
235
260
  <h3>📜 Recent History</h3>
236
261
  <div class="packages-list">{history_items}</div>
237
262
  </div>
238
- '''
263
+ """
239
264
 
240
265
  # Build "other packages" section (non-Quasarr)
241
- other_html = ''
266
+ other_html = ""
242
267
  other_count = len(other_queue) + len(other_history)
243
268
  if other_count > 0:
244
- other_items = ''
269
+ other_items = ""
245
270
  if other_queue:
246
- other_items += f'<h4>Queue ({len(other_queue)})</h4>'
247
- other_items += ''.join(_render_queue_item(item) for item in other_queue)
271
+ other_items += f"<h4>Queue ({len(other_queue)})</h4>"
272
+ other_items += "".join(_render_queue_item(item) for item in other_queue)
248
273
  if other_history:
249
- other_items += f'<h4>History ({len(other_history)})</h4>'
250
- other_items += ''.join(_render_history_item(item) for item in other_history[:5])
274
+ other_items += f"<h4>History ({len(other_history)})</h4>"
275
+ other_items += "".join(
276
+ _render_history_item(item) for item in other_history[:5]
277
+ )
251
278
 
252
- plural = 's' if other_count != 1 else ''
279
+ plural = "s" if other_count != 1 else ""
253
280
  # Only add separator class if there's Quasarr content above
254
- section_class = 'other-packages-section' if has_quasarr_content else 'other-packages-section no-separator'
281
+ section_class = (
282
+ "other-packages-section"
283
+ if has_quasarr_content
284
+ else "other-packages-section no-separator"
285
+ )
255
286
  other_html = f'''
256
287
  <div class="{section_class}">
257
288
  <details id="otherPackagesDetails">
@@ -262,33 +293,34 @@ def _render_packages_content():
262
293
  '''
263
294
 
264
295
  # Only show "no downloads" if there's literally nothing
265
- empty_html = ''
296
+ empty_html = ""
266
297
  if not has_any_content:
267
298
  empty_html = '<p class="empty-message">No packages</p>'
268
299
 
269
- return f'''
300
+ return f"""
270
301
  <div class="packages-container">
271
302
  {queue_html}
272
303
  {history_html}
273
304
  {other_html}
274
305
  {empty_html}
275
306
  </div>
276
- '''
307
+ """
277
308
 
278
309
 
279
310
  def setup_packages_routes(app):
280
- @app.get('/packages/delete/<package_id>')
311
+ @app.get("/packages/delete/<package_id>")
281
312
  def delete_package_route(package_id):
282
313
  success = delete_package(shared_state, package_id)
283
314
 
284
315
  # Redirect back to packages page with status message via query param
285
316
  from bottle import redirect
317
+
286
318
  if success:
287
- redirect('/packages?deleted=1')
319
+ redirect("/packages?deleted=1")
288
320
  else:
289
- redirect('/packages?deleted=0')
321
+ redirect("/packages?deleted=0")
290
322
 
291
- @app.get('/api/packages/content')
323
+ @app.get("/api/packages/content")
292
324
  def packages_content_api():
293
325
  """AJAX endpoint - returns just the packages content HTML for background refresh."""
294
326
  try:
@@ -297,17 +329,17 @@ def setup_packages_routes(app):
297
329
  device = None
298
330
 
299
331
  if not device:
300
- return '''
332
+ return """
301
333
  <div class="status-bar">
302
334
  <span class="status-pill error">
303
335
  ❌ JDownloader disconnected
304
336
  </span>
305
337
  </div>
306
- '''
338
+ """
307
339
 
308
340
  return _render_packages_content()
309
341
 
310
- @app.get('/packages')
342
+ @app.get("/packages")
311
343
  def packages_status():
312
344
  from bottle import request
313
345
 
@@ -317,7 +349,9 @@ def setup_packages_routes(app):
317
349
  device = None
318
350
 
319
351
  if not device:
320
- back_btn = render_button("Back", "secondary", {"onclick": "location.href='/'"})
352
+ back_btn = render_button(
353
+ "Back", "secondary", {"onclick": "location.href='/'"}
354
+ )
321
355
  return render_centered_html(f'''
322
356
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
323
357
  <div class="status-bar">
@@ -329,12 +363,14 @@ def setup_packages_routes(app):
329
363
  ''')
330
364
 
331
365
  # Check for delete status from redirect
332
- deleted = request.query.get('deleted')
366
+ deleted = request.query.get("deleted")
333
367
  status_message = ""
334
- if deleted == '1':
368
+ if deleted == "1":
335
369
  status_message = '<div class="status-message success">✅ Package deleted successfully.</div>'
336
- elif deleted == '0':
337
- status_message = '<div class="status-message error">❌ Failed to delete package.</div>'
370
+ elif deleted == "0":
371
+ status_message = (
372
+ '<div class="status-message error">❌ Failed to delete package.</div>'
373
+ )
338
374
 
339
375
  # Get rendered packages content using shared helper
340
376
  packages_content = _render_packages_content()
@@ -4,7 +4,7 @@
4
4
 
5
5
  import json
6
6
 
7
- from bottle import request, abort
7
+ from bottle import abort, request
8
8
 
9
9
  from quasarr.downloads import fail
10
10
  from quasarr.providers import shared_state
@@ -47,7 +47,7 @@ def setup_sponsors_helper_routes(app):
47
47
  package_id, data = selected_package
48
48
  title = data["title"]
49
49
  links = data["links"]
50
- mirror = None if (mirror := data.get('mirror')) == "None" else mirror
50
+ mirror = None if (mirror := data.get("mirror")) == "None" else mirror
51
51
  password = data["password"]
52
52
 
53
53
  rapid = [ln for ln in links if "rapidgator" in ln[1].lower()]
@@ -61,7 +61,7 @@ def setup_sponsors_helper_routes(app):
61
61
  "url": prioritized_links,
62
62
  "mirror": mirror,
63
63
  "password": password,
64
- "max_attempts": 3
64
+ "max_attempts": 3,
65
65
  }
66
66
  }
67
67
  except Exception as e:
@@ -72,22 +72,28 @@ def setup_sponsors_helper_routes(app):
72
72
  def download_api():
73
73
  try:
74
74
  data = request.json
75
- title = data.get('name')
76
- package_id = data.get('package_id')
77
- download_links = data.get('urls')
78
- password = data.get('password')
75
+ title = data.get("name")
76
+ package_id = data.get("package_id")
77
+ download_links = data.get("urls")
78
+ password = data.get("password")
79
79
 
80
80
  info(f"Received {len(download_links)} download links for {title}")
81
81
 
82
82
  if download_links:
83
- downloaded = shared_state.download_package(download_links, title, password, package_id)
83
+ downloaded = shared_state.download_package(
84
+ download_links, title, password, package_id
85
+ )
84
86
  if downloaded:
85
- StatsHelper(shared_state).increment_package_with_links(download_links)
87
+ StatsHelper(shared_state).increment_package_with_links(
88
+ download_links
89
+ )
86
90
  StatsHelper(shared_state).increment_captcha_decryptions_automatic()
87
91
  shared_state.get_db("protected").delete(package_id)
88
92
  send_discord_message(shared_state, title=title, case="solved")
89
93
  info(f"Download successfully started for {title}")
90
- return f"Downloaded {len(download_links)} download links for {title}"
94
+ return (
95
+ f"Downloaded {len(download_links)} download links for {title}"
96
+ )
91
97
  else:
92
98
  info(f"Download failed for {title}")
93
99
 
@@ -102,7 +108,7 @@ def setup_sponsors_helper_routes(app):
102
108
  def disable_api():
103
109
  try:
104
110
  data = request.json
105
- package_id = data.get('package_id')
111
+ package_id = data.get("package_id")
106
112
 
107
113
  if not package_id:
108
114
  return {"error": "Missing package_id"}, 400
@@ -111,11 +117,13 @@ def setup_sponsors_helper_routes(app):
111
117
 
112
118
  blob = shared_state.get_db("protected").retrieve(package_id)
113
119
  package_data = json.loads(blob)
114
- title = package_data.get('title')
120
+ title = package_data.get("title")
115
121
 
116
122
  package_data["disabled"] = True
117
123
 
118
- shared_state.get_db("protected").update_store(package_id, json.dumps(package_data))
124
+ shared_state.get_db("protected").update_store(
125
+ package_id, json.dumps(package_data)
126
+ )
119
127
 
120
128
  info(f"Disabled package {title}")
121
129
 
@@ -136,14 +144,19 @@ def setup_sponsors_helper_routes(app):
136
144
  StatsHelper(shared_state).increment_failed_decryptions_automatic()
137
145
 
138
146
  data = request.json
139
- package_id = data.get('package_id')
147
+ package_id = data.get("package_id")
140
148
 
141
149
  data = json.loads(shared_state.get_db("protected").retrieve(package_id))
142
- title = data.get('title')
150
+ title = data.get("title")
143
151
 
144
152
  if package_id:
145
153
  info(f'Marking package "{title}" with ID "{package_id}" as failed')
146
- failed = fail(title, package_id, shared_state, reason="Too many failed attempts by SponsorsHelper")
154
+ failed = fail(
155
+ title,
156
+ package_id,
157
+ shared_state,
158
+ reason="Too many failed attempts by SponsorsHelper",
159
+ )
147
160
  if failed:
148
161
  shared_state.get_db("protected").delete(package_id)
149
162
  send_discord_message(shared_state, title=title, case="failed")
@@ -8,7 +8,7 @@ from quasarr.providers.statistics import StatsHelper
8
8
 
9
9
 
10
10
  def setup_statistics(app, shared_state):
11
- @app.get('/statistics')
11
+ @app.get("/statistics")
12
12
  def statistics():
13
13
  stats_helper = StatsHelper(shared_state)
14
14
  stats = stats_helper.get_stats()
@@ -21,13 +21,13 @@ def setup_statistics(app, shared_state):
21
21
  <div class="stats-grid compact">
22
22
  <div class="stat-card highlight">
23
23
  <h3>📦 Total Download Attempts</h3>
24
- <div class="stat-value">{stats['total_download_attempts']:,}</div>
25
- <div class="stat-subtitle">Success Rate: {stats['download_success_rate']:,.1f}%</div>
24
+ <div class="stat-value">{stats["total_download_attempts"]:,}</div>
25
+ <div class="stat-subtitle">Success Rate: {stats["download_success_rate"]:,.1f}%</div>
26
26
  </div>
27
27
  <div class="stat-card highlight">
28
28
  <h3>🔐 Total CAPTCHA Decryptions</h3>
29
- <div class="stat-value">{stats['total_captcha_decryptions']:,}</div>
30
- <div class="stat-subtitle">Success Rate: {stats['decryption_success_rate']:,.1f}%</div>
29
+ <div class="stat-value">{stats["total_captcha_decryptions"]:,}</div>
30
+ <div class="stat-subtitle">Success Rate: {stats["decryption_success_rate"]:,.1f}%</div>
31
31
  </div>
32
32
  </div>
33
33
 
@@ -35,19 +35,19 @@ def setup_statistics(app, shared_state):
35
35
  <div class="stats-grid compact">
36
36
  <div class="stat-card">
37
37
  <h3>✅ Packages Downloaded</h3>
38
- <div class="stat-value">{stats['packages_downloaded']:,}</div>
38
+ <div class="stat-value">{stats["packages_downloaded"]:,}</div>
39
39
  </div>
40
40
  <div class="stat-card">
41
41
  <h3>⚙️ Links Processed</h3>
42
- <div class="stat-value">{stats['links_processed']:,}</div>
42
+ <div class="stat-value">{stats["links_processed"]:,}</div>
43
43
  </div>
44
44
  <div class="stat-card">
45
45
  <h3>❌ Failed Downloads</h3>
46
- <div class="stat-value">{stats['failed_downloads']:,}</div>
46
+ <div class="stat-value">{stats["failed_downloads"]:,}</div>
47
47
  </div>
48
48
  <div class="stat-card">
49
49
  <h3>🔗 Average Links per Package</h3>
50
- <div class="stat-value">{stats['average_links_per_package']:,.1f}</div>
50
+ <div class="stat-value">{stats["average_links_per_package"]:,.1f}</div>
51
51
  </div>
52
52
  </div>
53
53
 
@@ -55,21 +55,21 @@ def setup_statistics(app, shared_state):
55
55
  <div class="stats-grid compact">
56
56
  <div class="stat-card">
57
57
  <h3>🤖 Automatic Decryptions</h3>
58
- <div class="stat-value">{stats['captcha_decryptions_automatic']:,}</div>
59
- <div class="stat-subtitle">Success Rate: {stats['automatic_decryption_success_rate']:,.1f}%</div>
58
+ <div class="stat-value">{stats["captcha_decryptions_automatic"]:,}</div>
59
+ <div class="stat-subtitle">Success Rate: {stats["automatic_decryption_success_rate"]:,.1f}%</div>
60
60
  </div>
61
61
  <div class="stat-card">
62
62
  <h3>👤 Manual Decryptions</h3>
63
- <div class="stat-value">{stats['captcha_decryptions_manual']:,}</div>
64
- <div class="stat-subtitle">Success Rate: {stats['manual_decryption_success_rate']:,.1f}%</div>
63
+ <div class="stat-value">{stats["captcha_decryptions_manual"]:,}</div>
64
+ <div class="stat-subtitle">Success Rate: {stats["manual_decryption_success_rate"]:,.1f}%</div>
65
65
  </div>
66
66
  <div class="stat-card">
67
67
  <h3>⛔ Failed Auto Decryptions</h3>
68
- <div class="stat-value">{stats['failed_decryptions_automatic']:,}</div>
68
+ <div class="stat-value">{stats["failed_decryptions_automatic"]:,}</div>
69
69
  </div>
70
70
  <div class="stat-card">
71
71
  <h3>🚫 Failed Manual Decryptions</h3>
72
- <div class="stat-value">{stats['failed_decryptions_manual']:,}</div>
72
+ <div class="stat-value">{stats["failed_decryptions_manual"]:,}</div>
73
73
  </div>
74
74
  </div>
75
75
 
@@ -77,19 +77,19 @@ def setup_statistics(app, shared_state):
77
77
  <div class="stats-grid compact">
78
78
  <div class="stat-card">
79
79
  <h3>💾 Total Cached IDs</h3>
80
- <div class="stat-value">{stats['imdb_total_cached']:,}</div>
80
+ <div class="stat-value">{stats["imdb_total_cached"]:,}</div>
81
81
  </div>
82
82
  <div class="stat-card">
83
83
  <h3>🏷️ With Title</h3>
84
- <div class="stat-value">{stats['imdb_with_title']:,}</div>
84
+ <div class="stat-value">{stats["imdb_with_title"]:,}</div>
85
85
  </div>
86
86
  <div class="stat-card">
87
87
  <h3>🖼️ With Poster</h3>
88
- <div class="stat-value">{stats['imdb_with_poster']:,}</div>
88
+ <div class="stat-value">{stats["imdb_with_poster"]:,}</div>
89
89
  </div>
90
90
  <div class="stat-card">
91
91
  <h3>🌍 With Localized Title</h3>
92
- <div class="stat-value">{stats['imdb_with_localized']:,}</div>
92
+ <div class="stat-value">{stats["imdb_with_localized"]:,}</div>
93
93
  </div>
94
94
  </div>
95
95
  </div>