quasarr 2.4.7__py3-none-any.whl → 2.4.9__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.
- quasarr/__init__.py +134 -70
- quasarr/api/__init__.py +40 -31
- quasarr/api/arr/__init__.py +116 -108
- quasarr/api/captcha/__init__.py +262 -137
- quasarr/api/config/__init__.py +76 -46
- quasarr/api/packages/__init__.py +138 -102
- quasarr/api/sponsors_helper/__init__.py +29 -16
- quasarr/api/statistics/__init__.py +19 -19
- quasarr/downloads/__init__.py +165 -72
- quasarr/downloads/linkcrypters/al.py +35 -18
- quasarr/downloads/linkcrypters/filecrypt.py +107 -52
- quasarr/downloads/linkcrypters/hide.py +5 -6
- quasarr/downloads/packages/__init__.py +342 -177
- quasarr/downloads/sources/al.py +191 -100
- quasarr/downloads/sources/by.py +31 -13
- quasarr/downloads/sources/dd.py +27 -14
- quasarr/downloads/sources/dj.py +1 -3
- quasarr/downloads/sources/dl.py +126 -71
- quasarr/downloads/sources/dt.py +11 -5
- quasarr/downloads/sources/dw.py +28 -14
- quasarr/downloads/sources/he.py +32 -24
- quasarr/downloads/sources/mb.py +19 -9
- quasarr/downloads/sources/nk.py +14 -10
- quasarr/downloads/sources/nx.py +8 -18
- quasarr/downloads/sources/sf.py +45 -20
- quasarr/downloads/sources/sj.py +1 -3
- quasarr/downloads/sources/sl.py +9 -5
- quasarr/downloads/sources/wd.py +32 -12
- quasarr/downloads/sources/wx.py +35 -21
- quasarr/providers/auth.py +42 -37
- quasarr/providers/cloudflare.py +28 -30
- quasarr/providers/hostname_issues.py +2 -1
- quasarr/providers/html_images.py +2 -2
- quasarr/providers/html_templates.py +22 -14
- quasarr/providers/imdb_metadata.py +149 -80
- quasarr/providers/jd_cache.py +131 -39
- quasarr/providers/log.py +1 -1
- quasarr/providers/myjd_api.py +260 -196
- quasarr/providers/notifications.py +53 -41
- quasarr/providers/obfuscated.py +9 -4
- quasarr/providers/sessions/al.py +71 -55
- quasarr/providers/sessions/dd.py +21 -14
- quasarr/providers/sessions/dl.py +30 -19
- quasarr/providers/sessions/nx.py +23 -14
- quasarr/providers/shared_state.py +292 -141
- quasarr/providers/statistics.py +75 -43
- quasarr/providers/utils.py +33 -27
- quasarr/providers/version.py +45 -14
- quasarr/providers/web_server.py +10 -5
- quasarr/search/__init__.py +30 -18
- quasarr/search/sources/al.py +124 -73
- quasarr/search/sources/by.py +110 -59
- quasarr/search/sources/dd.py +57 -35
- quasarr/search/sources/dj.py +69 -48
- quasarr/search/sources/dl.py +159 -100
- quasarr/search/sources/dt.py +110 -74
- quasarr/search/sources/dw.py +121 -61
- quasarr/search/sources/fx.py +108 -62
- quasarr/search/sources/he.py +78 -49
- quasarr/search/sources/mb.py +96 -48
- quasarr/search/sources/nk.py +80 -50
- quasarr/search/sources/nx.py +91 -62
- quasarr/search/sources/sf.py +171 -106
- quasarr/search/sources/sj.py +69 -48
- quasarr/search/sources/sl.py +115 -71
- quasarr/search/sources/wd.py +67 -44
- quasarr/search/sources/wx.py +188 -123
- quasarr/storage/config.py +65 -52
- quasarr/storage/setup.py +238 -140
- quasarr/storage/sqlite_database.py +10 -4
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
- quasarr-2.4.9.dist-info/RECORD +81 -0
- quasarr-2.4.7.dist-info/RECORD +0 -81
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/api/packages/__init__.py
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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(
|
|
37
|
-
percentage = item.get(
|
|
38
|
-
timeleft = item.get(
|
|
39
|
-
bytes_val = item.get(
|
|
40
|
-
mb = item.get(
|
|
41
|
-
cat = item.get(
|
|
42
|
-
is_archive = item.get(
|
|
43
|
-
nzo_id = item.get(
|
|
44
|
-
storage = item.get(
|
|
45
|
-
|
|
46
|
-
is_captcha =
|
|
47
|
-
status_text =
|
|
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 =
|
|
51
|
-
elif
|
|
52
|
-
status_emoji =
|
|
53
|
-
status_text =
|
|
54
|
-
elif
|
|
55
|
-
status_emoji =
|
|
56
|
-
status_text =
|
|
57
|
-
elif
|
|
58
|
-
status_emoji =
|
|
59
|
-
status_text =
|
|
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 [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 =
|
|
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(
|
|
133
|
-
status = item.get(
|
|
134
|
-
bytes_val = item.get(
|
|
135
|
-
category = item.get(
|
|
136
|
-
is_archive = item.get(
|
|
137
|
-
extraction_status = item.get(
|
|
138
|
-
fail_message = item.get(
|
|
139
|
-
nzo_id = item.get(
|
|
140
|
-
storage = item.get(
|
|
141
|
-
|
|
142
|
-
is_error = status.lower() in [
|
|
143
|
-
card_class =
|
|
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 ==
|
|
151
|
-
archive_emoji =
|
|
152
|
-
elif extraction_status ==
|
|
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 =
|
|
158
|
-
error_html =
|
|
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 =
|
|
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(
|
|
205
|
-
history = downloads.get(
|
|
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(
|
|
209
|
-
other_queue = [p for p in queue if p.get(
|
|
210
|
-
quasarr_history = [p for p in history if p.get(
|
|
211
|
-
other_history = [p for p in history if p.get(
|
|
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 =
|
|
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 =
|
|
233
|
-
|
|
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
|
|
247
|
-
other_items +=
|
|
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
|
|
250
|
-
other_items +=
|
|
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 =
|
|
279
|
+
plural = "s" if other_count != 1 else ""
|
|
253
280
|
# Only add separator class if there's Quasarr content above
|
|
254
|
-
section_class =
|
|
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(
|
|
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(
|
|
319
|
+
redirect("/packages?deleted=1")
|
|
288
320
|
else:
|
|
289
|
-
redirect(
|
|
321
|
+
redirect("/packages?deleted=0")
|
|
290
322
|
|
|
291
|
-
@app.get(
|
|
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(
|
|
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(
|
|
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(
|
|
366
|
+
deleted = request.query.get("deleted")
|
|
333
367
|
status_message = ""
|
|
334
|
-
if deleted ==
|
|
368
|
+
if deleted == "1":
|
|
335
369
|
status_message = '<div class="status-message success">✅ Package deleted successfully.</div>'
|
|
336
|
-
elif deleted ==
|
|
337
|
-
status_message =
|
|
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
|
|
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(
|
|
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(
|
|
76
|
-
package_id = data.get(
|
|
77
|
-
download_links = data.get(
|
|
78
|
-
password = data.get(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
120
|
+
title = package_data.get("title")
|
|
115
121
|
|
|
116
122
|
package_data["disabled"] = True
|
|
117
123
|
|
|
118
|
-
shared_state.get_db("protected").update_store(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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[
|
|
25
|
-
<div class="stat-subtitle">Success Rate: {stats[
|
|
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[
|
|
30
|
-
<div class="stat-subtitle">Success Rate: {stats[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
59
|
-
<div class="stat-subtitle">Success Rate: {stats[
|
|
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[
|
|
64
|
-
<div class="stat-subtitle">Success Rate: {stats[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
92
|
+
<div class="stat-value">{stats["imdb_with_localized"]:,}</div>
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
</div>
|