quasarr 2.1.5__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- quasarr/api/__init__.py +94 -23
- quasarr/api/captcha/__init__.py +0 -12
- quasarr/api/config/__init__.py +22 -11
- quasarr/api/packages/__init__.py +26 -34
- quasarr/api/statistics/__init__.py +15 -15
- quasarr/downloads/__init__.py +9 -1
- quasarr/downloads/packages/__init__.py +2 -2
- quasarr/downloads/sources/al.py +6 -0
- quasarr/downloads/sources/by.py +29 -20
- quasarr/downloads/sources/dd.py +9 -1
- quasarr/downloads/sources/dl.py +3 -0
- quasarr/downloads/sources/dt.py +16 -7
- quasarr/downloads/sources/dw.py +22 -17
- quasarr/downloads/sources/he.py +11 -6
- quasarr/downloads/sources/mb.py +9 -3
- quasarr/downloads/sources/nk.py +9 -3
- quasarr/downloads/sources/nx.py +21 -17
- quasarr/downloads/sources/sf.py +21 -13
- quasarr/downloads/sources/sl.py +10 -2
- quasarr/downloads/sources/wd.py +18 -9
- quasarr/downloads/sources/wx.py +7 -11
- quasarr/providers/auth.py +1 -1
- quasarr/providers/cloudflare.py +1 -1
- quasarr/providers/hostname_issues.py +63 -0
- quasarr/providers/html_images.py +1 -18
- quasarr/providers/html_templates.py +104 -12
- quasarr/providers/obfuscated.py +11 -11
- quasarr/providers/sessions/al.py +27 -11
- quasarr/providers/sessions/dd.py +12 -4
- quasarr/providers/sessions/dl.py +19 -11
- quasarr/providers/sessions/nx.py +12 -4
- quasarr/providers/version.py +1 -1
- quasarr/search/sources/al.py +12 -1
- quasarr/search/sources/by.py +15 -4
- quasarr/search/sources/dd.py +22 -3
- quasarr/search/sources/dj.py +12 -1
- quasarr/search/sources/dl.py +12 -6
- quasarr/search/sources/dt.py +17 -4
- quasarr/search/sources/dw.py +15 -4
- quasarr/search/sources/fx.py +19 -6
- quasarr/search/sources/he.py +15 -2
- quasarr/search/sources/mb.py +15 -4
- quasarr/search/sources/nk.py +15 -2
- quasarr/search/sources/nx.py +15 -4
- quasarr/search/sources/sf.py +25 -8
- quasarr/search/sources/sj.py +14 -1
- quasarr/search/sources/sl.py +17 -2
- quasarr/search/sources/wd.py +15 -4
- quasarr/search/sources/wx.py +16 -18
- quasarr/storage/setup.py +150 -35
- {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/METADATA +6 -3
- quasarr-2.2.0.dist-info/RECORD +82 -0
- {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/WHEEL +1 -1
- quasarr-2.1.5.dist-info/RECORD +0 -81
- {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/entry_points.txt +0 -0
- {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/top_level.txt +0 -0
quasarr/api/__init__.py
CHANGED
|
@@ -13,9 +13,11 @@ from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
|
13
13
|
from quasarr.api.statistics import setup_statistics
|
|
14
14
|
from quasarr.providers import shared_state
|
|
15
15
|
from quasarr.providers.auth import add_auth_routes, add_auth_hook, show_logout_link
|
|
16
|
-
from quasarr.providers.
|
|
16
|
+
from quasarr.providers.hostname_issues import get_all_hostname_issues
|
|
17
|
+
from quasarr.providers.html_templates import render_button, render_centered_html, render_success
|
|
17
18
|
from quasarr.providers.web_server import Server
|
|
18
19
|
from quasarr.storage.config import Config
|
|
20
|
+
from quasarr.storage.sqlite_database import DataBase
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def get_api(shared_state_dict, shared_state_lock):
|
|
@@ -49,6 +51,50 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
49
51
|
except:
|
|
50
52
|
jd_connected = False
|
|
51
53
|
|
|
54
|
+
# Calculate hostname status
|
|
55
|
+
hostnames_config = Config('Hostnames')
|
|
56
|
+
skip_login_db = DataBase("skip_login")
|
|
57
|
+
hostname_issues = get_all_hostname_issues()
|
|
58
|
+
login_required_sites = ['al', 'dd', 'dl', 'nx']
|
|
59
|
+
|
|
60
|
+
working_count = 0
|
|
61
|
+
total_count = 0
|
|
62
|
+
|
|
63
|
+
for site_key in shared_state.values["sites"]:
|
|
64
|
+
shorthand = site_key.lower()
|
|
65
|
+
current_value = hostnames_config.get(shorthand)
|
|
66
|
+
|
|
67
|
+
# Skip unset hostnames and skipped logins
|
|
68
|
+
if not current_value:
|
|
69
|
+
continue
|
|
70
|
+
if shorthand in login_required_sites and skip_login_db.retrieve(shorthand):
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# This hostname counts toward total
|
|
74
|
+
total_count += 1
|
|
75
|
+
|
|
76
|
+
# Check if it's working (no issues)
|
|
77
|
+
if shorthand not in hostname_issues:
|
|
78
|
+
working_count += 1
|
|
79
|
+
|
|
80
|
+
# Determine status
|
|
81
|
+
if total_count == 0:
|
|
82
|
+
hostname_status_class = 'error'
|
|
83
|
+
hostname_status_emoji = '⚫️'
|
|
84
|
+
hostname_status_text = 'No hostnames configured'
|
|
85
|
+
elif working_count == 0:
|
|
86
|
+
hostname_status_class = 'error'
|
|
87
|
+
hostname_status_emoji = '🔴'
|
|
88
|
+
hostname_status_text = f'0/{total_count} hostnames operational'
|
|
89
|
+
elif working_count < total_count:
|
|
90
|
+
hostname_status_class = 'warning'
|
|
91
|
+
hostname_status_emoji = '🟡'
|
|
92
|
+
hostname_status_text = f'{working_count}/{total_count} hostnames operational'
|
|
93
|
+
else:
|
|
94
|
+
hostname_status_class = 'success'
|
|
95
|
+
hostname_status_emoji = '🟢'
|
|
96
|
+
hostname_status_text = f'{working_count}/{total_count} hostnames operational'
|
|
97
|
+
|
|
52
98
|
# CAPTCHA banner
|
|
53
99
|
captcha_hint = ""
|
|
54
100
|
if protected:
|
|
@@ -66,11 +112,14 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
66
112
|
</div>
|
|
67
113
|
"""
|
|
68
114
|
|
|
69
|
-
#
|
|
70
|
-
|
|
115
|
+
# Status bars
|
|
116
|
+
status_bars = f"""
|
|
71
117
|
<div class="status-bar">
|
|
72
118
|
<span class="status-pill {'success' if jd_connected else 'error'}">
|
|
73
|
-
{'✅' if jd_connected else '❌'} JDownloader {'
|
|
119
|
+
{'✅' if jd_connected else '❌'} JDownloader {'connected' if jd_connected else 'disconnected'}
|
|
120
|
+
</span>
|
|
121
|
+
<span class="status-pill {hostname_status_class}">
|
|
122
|
+
{hostname_status_emoji} {hostname_status_text}
|
|
74
123
|
</span>
|
|
75
124
|
</div>
|
|
76
125
|
"""
|
|
@@ -78,7 +127,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
78
127
|
info = f"""
|
|
79
128
|
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
80
129
|
|
|
81
|
-
{
|
|
130
|
+
{status_bars}
|
|
82
131
|
{captcha_hint}
|
|
83
132
|
|
|
84
133
|
<div class="quick-actions">
|
|
@@ -124,7 +173,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
124
173
|
</div>
|
|
125
174
|
|
|
126
175
|
<p style="margin-top: 15px;">
|
|
127
|
-
{render_button("Regenerate API key", "secondary", {"onclick": "
|
|
176
|
+
{render_button("Regenerate API key", "secondary", {"onclick": "confirmRegenerateApiKey()"})}
|
|
128
177
|
</p>
|
|
129
178
|
</div>
|
|
130
179
|
</details>
|
|
@@ -144,14 +193,27 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
144
193
|
margin-bottom: 20px;
|
|
145
194
|
flex-wrap: wrap;
|
|
146
195
|
}}
|
|
147
|
-
.status-
|
|
196
|
+
.status-pill {{
|
|
148
197
|
font-size: 0.9em;
|
|
149
|
-
padding:
|
|
150
|
-
border-radius:
|
|
151
|
-
|
|
198
|
+
padding: 8px 16px;
|
|
199
|
+
border-radius: 0.5rem;
|
|
200
|
+
font-weight: 500;
|
|
201
|
+
}}
|
|
202
|
+
.status-pill.success {{
|
|
203
|
+
background: var(--status-success-bg, #e8f5e9);
|
|
204
|
+
color: var(--status-success-color, #2e7d32);
|
|
205
|
+
border: 1px solid var(--status-success-border, #a5d6a7);
|
|
206
|
+
}}
|
|
207
|
+
.status-pill.warning {{
|
|
208
|
+
background: var(--status-warning-bg, #fff3e0);
|
|
209
|
+
color: var(--status-warning-color, #f57c00);
|
|
210
|
+
border: 1px solid var(--status-warning-border, #ffb74d);
|
|
211
|
+
}}
|
|
212
|
+
.status-pill.error {{
|
|
213
|
+
background: var(--status-error-bg, #ffebee);
|
|
214
|
+
color: var(--status-error-color, #c62828);
|
|
215
|
+
border: 1px solid var(--status-error-border, #ef9a9a);
|
|
152
216
|
}}
|
|
153
|
-
.status-ok {{ color: var(--status-ok, #2e7d32); }}
|
|
154
|
-
.status-error {{ color: var(--status-error, #c62828); }}
|
|
155
217
|
|
|
156
218
|
.alert {{
|
|
157
219
|
display: flex;
|
|
@@ -294,9 +356,15 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
294
356
|
/* Dark mode */
|
|
295
357
|
@media (prefers-color-scheme: dark) {{
|
|
296
358
|
:root {{
|
|
297
|
-
--status-bg: #
|
|
298
|
-
--status-
|
|
299
|
-
--status-
|
|
359
|
+
--status-success-bg: #1b5e20;
|
|
360
|
+
--status-success-color: #a5d6a7;
|
|
361
|
+
--status-success-border: #2e7d32;
|
|
362
|
+
--status-warning-bg: #3d3520;
|
|
363
|
+
--status-warning-color: #ffb74d;
|
|
364
|
+
--status-warning-border: #d69e2e;
|
|
365
|
+
--status-error-bg: #b71c1c;
|
|
366
|
+
--status-error-color: #ef9a9a;
|
|
367
|
+
--status-error-border: #c62828;
|
|
300
368
|
--alert-warning-bg: #3d3520;
|
|
301
369
|
--alert-warning-border: #d69e2e;
|
|
302
370
|
--card-bg: #2d3748;
|
|
@@ -356,7 +424,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
356
424
|
if (callback) callback();
|
|
357
425
|
}}, 1500);
|
|
358
426
|
}} catch (e) {{
|
|
359
|
-
|
|
427
|
+
showModal('Error', 'Copy failed. Please copy manually.');
|
|
360
428
|
}}
|
|
361
429
|
document.body.removeChild(textarea);
|
|
362
430
|
}}
|
|
@@ -389,6 +457,15 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
389
457
|
}};
|
|
390
458
|
}}
|
|
391
459
|
}})();
|
|
460
|
+
|
|
461
|
+
function confirmRegenerateApiKey() {{
|
|
462
|
+
showModal(
|
|
463
|
+
'Regenerate API key?',
|
|
464
|
+
'Are you sure you want to regenerate the API key? This will invalidate the current key.',
|
|
465
|
+
`<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
|
466
|
+
<button class="btn-primary" onclick="location.href='/regenerate-api-key'">Regenerate</button>`
|
|
467
|
+
);
|
|
468
|
+
}}
|
|
392
469
|
</script>
|
|
393
470
|
"""
|
|
394
471
|
# Add logout link for form auth
|
|
@@ -397,12 +474,6 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
397
474
|
|
|
398
475
|
@app.get('/regenerate-api-key')
|
|
399
476
|
def regenerate_api_key():
|
|
400
|
-
|
|
401
|
-
return f"""
|
|
402
|
-
<script>
|
|
403
|
-
alert('API key replaced with: {api_key}');
|
|
404
|
-
window.location.href = '/';
|
|
405
|
-
</script>
|
|
406
|
-
"""
|
|
477
|
+
return render_success(f'API key replaced!', 5)
|
|
407
478
|
|
|
408
479
|
Server(app, listen='0.0.0.0', port=shared_state.values["port"]).serve_forever()
|
quasarr/api/captcha/__init__.py
CHANGED
|
@@ -1211,18 +1211,6 @@ def setup_captcha_routes(app):
|
|
|
1211
1211
|
|
|
1212
1212
|
content = render_centered_html(r'''
|
|
1213
1213
|
<style>
|
|
1214
|
-
@media (max-width: 600px) {
|
|
1215
|
-
.package-selector,
|
|
1216
|
-
#failed-attempts-warning {
|
|
1217
|
-
margin-left: 0 !important;
|
|
1218
|
-
margin-right: 0 !important;
|
|
1219
|
-
padding-left: 8px !important;
|
|
1220
|
-
padding-right: 8px !important;
|
|
1221
|
-
border-radius: 0 !important;
|
|
1222
|
-
border-left: none !important;
|
|
1223
|
-
border-right: none !important;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
1214
|
/* Fix captcha container to shrink-wrap iframe on desktop */
|
|
1227
1215
|
.captcha-container {
|
|
1228
1216
|
display: inline-block;
|
quasarr/api/config/__init__.py
CHANGED
|
@@ -22,6 +22,12 @@ from quasarr.storage.sqlite_database import DataBase
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def setup_config(app, shared_state):
|
|
25
|
+
@app.get("/api/hostname-issues")
|
|
26
|
+
def get_hostname_issues_api():
|
|
27
|
+
response.content_type = 'application/json'
|
|
28
|
+
from quasarr.providers.hostname_issues import get_all_hostname_issues
|
|
29
|
+
return {"issues": get_all_hostname_issues()}
|
|
30
|
+
|
|
25
31
|
@app.get('/hostnames')
|
|
26
32
|
def hostnames_ui():
|
|
27
33
|
message = """<p>
|
|
@@ -167,18 +173,23 @@ def setup_config(app, shared_state):
|
|
|
167
173
|
return true;
|
|
168
174
|
}}
|
|
169
175
|
function confirmRestart() {{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
showModal('Restart Quasarr?', 'Are you sure you want to restart Quasarr now?',
|
|
177
|
+
`<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
|
178
|
+
<button class="btn-primary" onclick="performRestart()">Restart</button>`
|
|
179
|
+
);
|
|
180
|
+
}}
|
|
181
|
+
function performRestart() {{
|
|
182
|
+
closeModal();
|
|
183
|
+
fetch('/api/restart', {{ method: 'POST' }})
|
|
184
|
+
.then(response => response.json())
|
|
185
|
+
.then(data => {{
|
|
186
|
+
if (data.success) {{
|
|
179
187
|
showRestartOverlay();
|
|
180
|
-
}}
|
|
181
|
-
}}
|
|
188
|
+
}}
|
|
189
|
+
}})
|
|
190
|
+
.catch(error => {{
|
|
191
|
+
showRestartOverlay();
|
|
192
|
+
}});
|
|
182
193
|
}}
|
|
183
194
|
function showRestartOverlay() {{
|
|
184
195
|
document.body.innerHTML = `
|
quasarr/api/packages/__init__.py
CHANGED
|
@@ -309,21 +309,6 @@ def setup_packages_routes(app):
|
|
|
309
309
|
|
|
310
310
|
<p>{back_btn}</p>
|
|
311
311
|
|
|
312
|
-
<!-- Delete confirmation modal -->
|
|
313
|
-
<div class="modal" id="deleteModal">
|
|
314
|
-
<div class="modal-content">
|
|
315
|
-
<h3>🗑️ Delete Package?</h3>
|
|
316
|
-
<p class="modal-package-name" id="modalPackageName"></p>
|
|
317
|
-
<div class="modal-warning">
|
|
318
|
-
<strong>⛔ Warning:</strong> This will permanently delete the package AND all associated files from disk. This action cannot be undone!
|
|
319
|
-
</div>
|
|
320
|
-
<div class="modal-buttons">
|
|
321
|
-
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
|
322
|
-
<button class="btn-danger" id="confirmDeleteBtn">🗑️ Delete Package & Files</button>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
</div>
|
|
326
|
-
|
|
327
312
|
<style>
|
|
328
313
|
.packages-container {{ max-width: 600px; margin: 0 auto; }}
|
|
329
314
|
.section {{ margin: 20px 0; }}
|
|
@@ -412,14 +397,6 @@ def setup_packages_routes(app):
|
|
|
412
397
|
border: 1px solid var(--error-border, #f1aeb5);
|
|
413
398
|
}}
|
|
414
399
|
|
|
415
|
-
/* Modal */
|
|
416
|
-
.modal {{ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }}
|
|
417
|
-
.modal.show {{ display: flex; }}
|
|
418
|
-
.modal-content {{ background: var(--modal-bg, white); padding: 25px; border-radius: 12px; max-width: 400px; width: 90%; text-align: center; }}
|
|
419
|
-
.modal-content h3 {{ margin: 0 0 15px 0; color: var(--error-msg-color, #c62828); }}
|
|
420
|
-
.modal-package-name {{ font-weight: 500; word-break: break-word; padding: 10px; background: var(--code-bg, #f5f5f5); border-radius: 6px; margin: 10px 0; }}
|
|
421
|
-
.modal-warning {{ background: var(--error-msg-bg, #ffebee); color: var(--error-msg-color, #c62828); padding: 12px; border-radius: 6px; margin: 15px 0; font-size: 0.9em; text-align: left; }}
|
|
422
|
-
.modal-buttons {{ display: flex; gap: 10px; justify-content: center; margin-top: 20px; }}
|
|
423
400
|
.btn-danger {{ background: var(--btn-danger-bg, #dc3545); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 500; }}
|
|
424
401
|
.btn-danger:hover {{ opacity: 0.9; }}
|
|
425
402
|
|
|
@@ -544,20 +521,35 @@ def setup_packages_routes(app):
|
|
|
544
521
|
let deletePackageId = null;
|
|
545
522
|
function confirmDelete(packageId, packageName) {{
|
|
546
523
|
deletePackageId = packageId;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
524
|
+
|
|
525
|
+
const content = `
|
|
526
|
+
<p class="modal-package-name" style="font-weight: 500; word-break: break-word; padding: 10px; background: var(--code-bg, #f5f5f5); border-radius: 6px; margin: 10px 0;">${{packageName}}</p>
|
|
527
|
+
<div class="modal-warning" style="background: var(--error-msg-bg, #ffebee); color: var(--error-msg-color, #c62828); padding: 12px; border-radius: 6px; margin: 15px 0; font-size: 0.9em; text-align: left;">
|
|
528
|
+
<strong>⛔ Warning:</strong> This will permanently delete the package AND all associated files from disk. This action cannot be undone!
|
|
529
|
+
</div>
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
const buttons = `
|
|
533
|
+
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
|
534
|
+
<button class="btn-danger" onclick="performDelete()">🗑️ Delete Package & Files</button>
|
|
535
|
+
`;
|
|
536
|
+
|
|
537
|
+
showModal('🗑️ Delete Package?', content, buttons);
|
|
538
|
+
refreshPaused = true;
|
|
550
539
|
}}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
deletePackageId
|
|
554
|
-
|
|
540
|
+
|
|
541
|
+
function performDelete() {{
|
|
542
|
+
if (deletePackageId) {{
|
|
543
|
+
location.href = '/packages/delete/' + encodeURIComponent(deletePackageId);
|
|
544
|
+
}}
|
|
555
545
|
}}
|
|
556
|
-
|
|
557
|
-
|
|
546
|
+
|
|
547
|
+
// Hook into modal closing to resume refresh
|
|
548
|
+
const baseCloseModal = window.closeModal;
|
|
549
|
+
window.closeModal = function() {{
|
|
550
|
+
if (baseCloseModal) baseCloseModal();
|
|
551
|
+
refreshPaused = false;
|
|
558
552
|
}};
|
|
559
|
-
document.getElementById('deleteModal').onclick = function(e) {{ if (e.target === this) closeModal(); }};
|
|
560
|
-
document.addEventListener('keydown', function(e) {{ if (e.key === 'Escape') closeModal(); }});
|
|
561
553
|
</script>
|
|
562
554
|
'''
|
|
563
555
|
|
|
@@ -21,33 +21,33 @@ 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']
|
|
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']
|
|
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
|
|
|
34
|
-
<h3
|
|
34
|
+
<h3>⬇️ Downloads</h3>
|
|
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']
|
|
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']
|
|
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']
|
|
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
|
</div>
|
quasarr/downloads/__init__.py
CHANGED
|
@@ -24,6 +24,7 @@ from quasarr.downloads.sources.sj import get_sj_download_links
|
|
|
24
24
|
from quasarr.downloads.sources.sl import get_sl_download_links
|
|
25
25
|
from quasarr.downloads.sources.wd import get_wd_download_links
|
|
26
26
|
from quasarr.downloads.sources.wx import get_wx_download_links
|
|
27
|
+
from quasarr.providers.hostname_issues import mark_hostname_issue, clear_hostname_issue
|
|
27
28
|
from quasarr.providers.log import info
|
|
28
29
|
from quasarr.providers.notifications import send_discord_message
|
|
29
30
|
from quasarr.providers.statistics import StatsHelper
|
|
@@ -345,7 +346,14 @@ def download(shared_state, request_from, title, url, mirror, size_mb, password,
|
|
|
345
346
|
for key, getter in SOURCE_GETTERS.items():
|
|
346
347
|
hostname = config.get(key)
|
|
347
348
|
if hostname and hostname.lower() in url.lower():
|
|
348
|
-
|
|
349
|
+
try:
|
|
350
|
+
source_result = getter(shared_state, url, mirror, title, password)
|
|
351
|
+
if source_result and source_result.get("links"):
|
|
352
|
+
clear_hostname_issue(key)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
info(f"Error getting download links from {key.upper()}: {e}")
|
|
355
|
+
mark_hostname_issue(key, "download", str(e))
|
|
356
|
+
source_result = None
|
|
349
357
|
label = key.upper()
|
|
350
358
|
detected_source_key = key
|
|
351
359
|
break
|
|
@@ -639,12 +639,12 @@ def get_packages(shared_state, _cache=None):
|
|
|
639
639
|
debug(
|
|
640
640
|
f" -> {item['percentage']}% | {item['timeleft']} | {size_str} | {item['cat']} {archive_indicator}")
|
|
641
641
|
for item in downloads['history']:
|
|
642
|
-
status_icon = "
|
|
642
|
+
status_icon = "✅" if item['status'] == 'Completed' else "✗"
|
|
643
643
|
is_archive = item.get('is_archive')
|
|
644
644
|
extraction_ok = item.get('extraction_ok', False)
|
|
645
645
|
# Only show archive status if we know it's an archive
|
|
646
646
|
if is_archive:
|
|
647
|
-
archive_status = f"[ARCHIVE: {'EXTRACTED
|
|
647
|
+
archive_status = f"[ARCHIVE: {'EXTRACTED ✅' if extraction_ok else 'NOT EXTRACTED'}]"
|
|
648
648
|
else:
|
|
649
649
|
archive_status = ""
|
|
650
650
|
# Format size
|
quasarr/downloads/sources/al.py
CHANGED
|
@@ -13,6 +13,7 @@ from urllib.parse import urlparse
|
|
|
13
13
|
from bs4 import BeautifulSoup
|
|
14
14
|
|
|
15
15
|
from quasarr.downloads.linkcrypters.al import decrypt_content, solve_captcha
|
|
16
|
+
from quasarr.providers.hostname_issues import mark_hostname_issue
|
|
16
17
|
from quasarr.providers.log import info, debug
|
|
17
18
|
from quasarr.providers.sessions.al import retrieve_and_validate_session, invalidate_session, unwrap_flaresolverr_body, \
|
|
18
19
|
fetch_via_flaresolverr, fetch_via_requests_session
|
|
@@ -525,6 +526,7 @@ def check_release(shared_state, details_html, release_id, title, episode_in_titl
|
|
|
525
526
|
return guessed_title, release_id
|
|
526
527
|
except Exception as e:
|
|
527
528
|
info(f"Error guessing release title from release: {e}")
|
|
529
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
528
530
|
|
|
529
531
|
return title, release_id
|
|
530
532
|
|
|
@@ -566,6 +568,7 @@ def get_al_download_links(shared_state, url, mirror, title, password):
|
|
|
566
568
|
sess = retrieve_and_validate_session(shared_state)
|
|
567
569
|
if not sess:
|
|
568
570
|
info(f"Could not retrieve valid session for {al}")
|
|
571
|
+
mark_hostname_issue(hostname, "download", "Session error")
|
|
569
572
|
return {}
|
|
570
573
|
|
|
571
574
|
details_page = fetch_via_flaresolverr(shared_state, "GET", url, timeout=30)
|
|
@@ -693,6 +696,7 @@ def get_al_download_links(shared_state, url, mirror, title, password):
|
|
|
693
696
|
|
|
694
697
|
except RuntimeError as e:
|
|
695
698
|
info(f"Error solving CAPTCHA: {e}")
|
|
699
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
696
700
|
else:
|
|
697
701
|
info(f"CAPTCHA solver returned invalid solution, retrying... (attempt {tries})")
|
|
698
702
|
|
|
@@ -710,8 +714,10 @@ def get_al_download_links(shared_state, url, mirror, title, password):
|
|
|
710
714
|
debug(f"Decrypted URLs: {links}")
|
|
711
715
|
except Exception as e:
|
|
712
716
|
info(f"Error during decryption: {e}")
|
|
717
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
713
718
|
except Exception as e:
|
|
714
719
|
info(f"Error loading AL download: {e}")
|
|
720
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
715
721
|
invalidate_session(shared_state)
|
|
716
722
|
|
|
717
723
|
success = bool(links)
|
quasarr/downloads/sources/by.py
CHANGED
|
@@ -10,8 +10,11 @@ from urllib.parse import urlparse
|
|
|
10
10
|
import requests
|
|
11
11
|
from bs4 import BeautifulSoup
|
|
12
12
|
|
|
13
|
+
from quasarr.providers.hostname_issues import mark_hostname_issue
|
|
13
14
|
from quasarr.providers.log import info, debug
|
|
14
15
|
|
|
16
|
+
hostname = "by"
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
def get_by_download_links(shared_state, url, mirror, title, password):
|
|
17
20
|
"""
|
|
@@ -29,9 +32,9 @@ def get_by_download_links(shared_state, url, mirror, title, password):
|
|
|
29
32
|
links = []
|
|
30
33
|
|
|
31
34
|
try:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
soup = BeautifulSoup(
|
|
35
|
+
r = requests.get(url, headers=headers, timeout=10)
|
|
36
|
+
r.raise_for_status()
|
|
37
|
+
soup = BeautifulSoup(r.text, "html.parser")
|
|
35
38
|
frames = [iframe.get("src") for iframe in soup.find_all("iframe") if iframe.get("src")]
|
|
36
39
|
|
|
37
40
|
frame_urls = [src for src in frames if f'https://{by}' in src]
|
|
@@ -43,10 +46,12 @@ def get_by_download_links(shared_state, url, mirror, title, password):
|
|
|
43
46
|
|
|
44
47
|
def fetch(url):
|
|
45
48
|
try:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
rq = requests.get(url, headers=headers, timeout=10)
|
|
50
|
+
rq.raise_for_status()
|
|
51
|
+
return rq.text, url
|
|
52
|
+
except Exception as e:
|
|
49
53
|
info(f"Error fetching iframe URL: {url}")
|
|
54
|
+
mark_hostname_issue(hostname, "download", str(e))
|
|
50
55
|
return None, url
|
|
51
56
|
|
|
52
57
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
|
@@ -70,43 +75,47 @@ def get_by_download_links(shared_state, url, mirror, title, password):
|
|
|
70
75
|
continue
|
|
71
76
|
|
|
72
77
|
href = link["href"]
|
|
73
|
-
|
|
74
|
-
hostname_lower =
|
|
78
|
+
link_hostname = link.text.strip().replace(" ", "")
|
|
79
|
+
hostname_lower = link_hostname.lower()
|
|
75
80
|
|
|
76
81
|
if mirror_lower and mirror_lower not in hostname_lower:
|
|
77
|
-
debug(f'Skipping link from "{
|
|
82
|
+
debug(f'Skipping link from "{link_hostname}" (not the desired mirror "{mirror}")!')
|
|
78
83
|
continue
|
|
79
84
|
|
|
80
|
-
url_hosters.append((href,
|
|
85
|
+
url_hosters.append((href, link_hostname))
|
|
81
86
|
|
|
82
87
|
def resolve_redirect(href_hostname):
|
|
83
88
|
href, hostname = href_hostname
|
|
84
89
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
rq = requests.get(href, headers=headers, timeout=10, allow_redirects=True)
|
|
91
|
+
rq.raise_for_status()
|
|
92
|
+
if "/404.html" in rq.url:
|
|
87
93
|
info(f"Link leads to 404 page for {hostname}: {r.url}")
|
|
88
94
|
return None
|
|
89
95
|
time.sleep(1)
|
|
90
|
-
return
|
|
96
|
+
return rq.url
|
|
91
97
|
except Exception as e:
|
|
92
98
|
info(f"Error resolving link for {hostname}: {e}")
|
|
99
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
93
100
|
return None
|
|
94
101
|
|
|
95
102
|
for pair in url_hosters:
|
|
96
103
|
resolved_url = resolve_redirect(pair)
|
|
97
|
-
|
|
104
|
+
link_hostname = pair[1]
|
|
98
105
|
|
|
99
|
-
if not
|
|
100
|
-
|
|
106
|
+
if not link_hostname:
|
|
107
|
+
link_hostname = urlparse(resolved_url).hostname
|
|
101
108
|
|
|
102
|
-
if resolved_url and
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
if resolved_url and link_hostname and link_hostname.startswith(
|
|
110
|
+
("ddownload", "rapidgator", "turbobit", "filecrypt")):
|
|
111
|
+
if "rapidgator" in link_hostname:
|
|
112
|
+
links.insert(0, [resolved_url, link_hostname])
|
|
105
113
|
else:
|
|
106
|
-
links.append([resolved_url,
|
|
114
|
+
links.append([resolved_url, link_hostname])
|
|
107
115
|
|
|
108
116
|
|
|
109
117
|
except Exception as e:
|
|
110
118
|
info(f"Error loading BY download links: {e}")
|
|
119
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
111
120
|
|
|
112
121
|
return {"links": links}
|
quasarr/downloads/sources/dd.py
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
# Quasarr
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
|
+
from quasarr.providers.hostname_issues import mark_hostname_issue
|
|
5
6
|
from quasarr.providers.log import info, debug
|
|
6
7
|
from quasarr.providers.sessions.dd import create_and_persist_session, retrieve_and_validate_session
|
|
7
8
|
|
|
9
|
+
hostname = "dd"
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
def get_dd_download_links(shared_state, url, mirror, title, password):
|
|
10
13
|
"""
|
|
@@ -18,6 +21,7 @@ def get_dd_download_links(shared_state, url, mirror, title, password):
|
|
|
18
21
|
dd_session = retrieve_and_validate_session(shared_state)
|
|
19
22
|
if not dd_session:
|
|
20
23
|
info(f"Could not retrieve valid session for {dd}")
|
|
24
|
+
mark_hostname_issue(hostname, "download", "Session error")
|
|
21
25
|
return {"links": []}
|
|
22
26
|
|
|
23
27
|
links = []
|
|
@@ -43,7 +47,9 @@ def get_dd_download_links(shared_state, url, mirror, title, password):
|
|
|
43
47
|
for page in range(0, 100, 20):
|
|
44
48
|
api_url = f'https://{dd}/index/search/keyword/{title}/qualities/{",".join(qualities)}/from/{page}/search'
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
r = dd_session.get(api_url, headers=headers, timeout=10)
|
|
51
|
+
r.raise_for_status()
|
|
52
|
+
releases_on_page = r.json()
|
|
47
53
|
if releases_on_page:
|
|
48
54
|
release_list.extend(releases_on_page)
|
|
49
55
|
|
|
@@ -75,9 +81,11 @@ def get_dd_download_links(shared_state, url, mirror, title, password):
|
|
|
75
81
|
break
|
|
76
82
|
except Exception as e:
|
|
77
83
|
info(f"Error parsing DD download: {e}")
|
|
84
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
78
85
|
continue
|
|
79
86
|
|
|
80
87
|
except Exception as e:
|
|
81
88
|
info(f"Error loading DD download: {e}")
|
|
89
|
+
mark_hostname_issue(hostname, "download", str(e) if "e" in dir() else "Download error")
|
|
82
90
|
|
|
83
91
|
return {"links": links}
|