quasarr 1.16.10__py3-none-any.whl → 1.17.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/captcha/__init__.py +153 -5
- quasarr/downloads/linkcrypters/filecrypt.py +88 -21
- quasarr/providers/version.py +1 -1
- {quasarr-1.16.10.dist-info → quasarr-1.17.0.dist-info}/METADATA +1 -1
- {quasarr-1.16.10.dist-info → quasarr-1.17.0.dist-info}/RECORD +9 -9
- {quasarr-1.16.10.dist-info → quasarr-1.17.0.dist-info}/WHEEL +0 -0
- {quasarr-1.16.10.dist-info → quasarr-1.17.0.dist-info}/entry_points.txt +0 -0
- {quasarr-1.16.10.dist-info → quasarr-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.16.10.dist-info → quasarr-1.17.0.dist-info}/top_level.txt +0 -0
quasarr/api/captcha/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ import requests
|
|
|
11
11
|
from bottle import request, response, redirect
|
|
12
12
|
|
|
13
13
|
import quasarr.providers.html_images as images
|
|
14
|
-
from quasarr.downloads.linkcrypters.filecrypt import get_filecrypt_links
|
|
14
|
+
from quasarr.downloads.linkcrypters.filecrypt import get_filecrypt_links, DLC
|
|
15
15
|
from quasarr.downloads.packages import delete_package
|
|
16
16
|
from quasarr.providers import shared_state
|
|
17
17
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
@@ -65,13 +65,17 @@ def setup_captcha_routes(app):
|
|
|
65
65
|
others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
|
|
66
66
|
prioritized_links = rapid + others
|
|
67
67
|
|
|
68
|
+
# This is required for bypass on circlecaptcha
|
|
69
|
+
original_url = data.get("original_url", "")
|
|
70
|
+
|
|
68
71
|
payload = {
|
|
69
72
|
"package_id": package_id,
|
|
70
73
|
"title": title,
|
|
71
74
|
"password": password,
|
|
72
75
|
"mirror": desired_mirror,
|
|
73
76
|
"session": session,
|
|
74
|
-
"links": prioritized_links
|
|
77
|
+
"links": prioritized_links,
|
|
78
|
+
"original_url": original_url
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
encoded_payload = urlsafe_b64encode(json.dumps(payload).encode()).decode()
|
|
@@ -97,6 +101,34 @@ def setup_captcha_routes(app):
|
|
|
97
101
|
except Exception as e:
|
|
98
102
|
return {"error": f"Failed to decode payload: {str(e)}"}
|
|
99
103
|
|
|
104
|
+
def render_bypass_section(url, package_id, title, password):
|
|
105
|
+
"""Render the bypass UI section for both cutcaptcha and circle captcha pages"""
|
|
106
|
+
return f'''
|
|
107
|
+
<div style="margin-top: 40px; padding-top: 20px; border-top: 2px solid #ccc;">
|
|
108
|
+
<h3>Bypass CAPTCHA</h3>
|
|
109
|
+
<a href="{url}" target="_blank">Protected Link</a>
|
|
110
|
+
<form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
|
|
111
|
+
<input type="hidden" name="package_id" value="{package_id}" />
|
|
112
|
+
<input type="hidden" name="title" value="{title}" />
|
|
113
|
+
<input type="hidden" name="password" value="{password}" />
|
|
114
|
+
|
|
115
|
+
<div style="margin-bottom: 15px;">
|
|
116
|
+
<label for="links-input"><b>Paste direct download links (one per line):</b></label><br>
|
|
117
|
+
<textarea id="links-input" name="links" rows="5" style="width: 100%; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div style="margin-bottom: 15px;">
|
|
121
|
+
<label for="dlc-file"><b>Or upload DLC file:</b></label><br>
|
|
122
|
+
<input type="file" id="dlc-file" name="dlc_file" accept=".dlc" />
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div>
|
|
126
|
+
{render_button("Submit Bypass", "primary", {"type": "submit"})}
|
|
127
|
+
</div>
|
|
128
|
+
</form>
|
|
129
|
+
</div>
|
|
130
|
+
'''
|
|
131
|
+
|
|
100
132
|
@app.get('/captcha/delete/<package_id>')
|
|
101
133
|
def delete_captcha_package(package_id):
|
|
102
134
|
success = delete_package(shared_state, package_id)
|
|
@@ -188,6 +220,11 @@ def setup_captcha_routes(app):
|
|
|
188
220
|
solve_another_html = render_button("Solve another CAPTCHA", "primary", {"onclick": "location.href='/captcha'"})
|
|
189
221
|
back_button_html = render_button("Back", "secondary", {"onclick": "location.href='/'"})
|
|
190
222
|
|
|
223
|
+
url = prioritized_links[0][0]
|
|
224
|
+
|
|
225
|
+
# Add bypass section
|
|
226
|
+
bypass_section = render_bypass_section(url, package_id, title, password)
|
|
227
|
+
|
|
191
228
|
content = render_centered_html(r'''
|
|
192
229
|
<script type="text/javascript">
|
|
193
230
|
var api_key = "''' + captcha_values()["api_key"] + r'''";
|
|
@@ -200,6 +237,7 @@ def setup_captcha_routes(app):
|
|
|
200
237
|
document.getElementById("mirrors-select").remove();
|
|
201
238
|
document.getElementById("delete-package-section").style.display = "none";
|
|
202
239
|
document.getElementById("back-button-section").style.display = "none";
|
|
240
|
+
document.getElementById("bypass-section").style.display = "none";
|
|
203
241
|
|
|
204
242
|
// Remove width limit on result screen
|
|
205
243
|
var packageTitle = document.getElementById("package-title");
|
|
@@ -247,6 +285,7 @@ def setup_captcha_routes(app):
|
|
|
247
285
|
<div>
|
|
248
286
|
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
249
287
|
<p id="package-title" style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
|
|
288
|
+
<h3>Solve CAPTCHA</h3>
|
|
250
289
|
<div id="captcha-key"></div>
|
|
251
290
|
{link_select}<br><br>
|
|
252
291
|
<input type="hidden" id="link-hidden" value="{prioritized_links[0][0]}" />
|
|
@@ -267,6 +306,9 @@ def setup_captcha_routes(app):
|
|
|
267
306
|
<p>
|
|
268
307
|
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
269
308
|
</p>
|
|
309
|
+
</div>
|
|
310
|
+
<div id="bypass-section">
|
|
311
|
+
{bypass_section}
|
|
270
312
|
</div>
|
|
271
313
|
</div>
|
|
272
314
|
</html>''')
|
|
@@ -346,6 +388,104 @@ def setup_captcha_routes(app):
|
|
|
346
388
|
response.set_header(header, resp.headers[header])
|
|
347
389
|
return resp.content
|
|
348
390
|
|
|
391
|
+
@app.post('/captcha/bypass-submit')
|
|
392
|
+
def handle_bypass_submit():
|
|
393
|
+
"""Handle bypass submission with either links or DLC file"""
|
|
394
|
+
try:
|
|
395
|
+
package_id = request.forms.get('package_id')
|
|
396
|
+
title = request.forms.get('title')
|
|
397
|
+
password = request.forms.get('password', '')
|
|
398
|
+
links_input = request.forms.get('links', '').strip()
|
|
399
|
+
dlc_upload = request.files.get('dlc_file')
|
|
400
|
+
|
|
401
|
+
if not package_id or not title:
|
|
402
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
403
|
+
<p><b>Error:</b> Missing package information.</p>
|
|
404
|
+
<p>
|
|
405
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
406
|
+
</p>''')
|
|
407
|
+
|
|
408
|
+
# Process links input
|
|
409
|
+
if links_input:
|
|
410
|
+
info(f"Processing direct links bypass for {title}")
|
|
411
|
+
links = [link.strip() for link in links_input.split('\n') if link.strip()]
|
|
412
|
+
info(f"Received {len(links)} direct download links")
|
|
413
|
+
|
|
414
|
+
# Process DLC file
|
|
415
|
+
elif dlc_upload:
|
|
416
|
+
info(f"Processing DLC file bypass for {title}")
|
|
417
|
+
dlc_content = dlc_upload.file.read()
|
|
418
|
+
try:
|
|
419
|
+
decrypted_links = DLC(shared_state, dlc_content).decrypt()
|
|
420
|
+
if decrypted_links:
|
|
421
|
+
links = decrypted_links
|
|
422
|
+
info(f"Decrypted {len(links)} links from DLC file")
|
|
423
|
+
else:
|
|
424
|
+
raise ValueError("DLC decryption returned no links")
|
|
425
|
+
except Exception as e:
|
|
426
|
+
info(f"DLC decryption failed: {e}")
|
|
427
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
428
|
+
<p><b>Error:</b> Failed to decrypt DLC file: {str(e)}</p>
|
|
429
|
+
<p>
|
|
430
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
431
|
+
</p>''')
|
|
432
|
+
else:
|
|
433
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
434
|
+
<p><b>Error:</b> Please provide either links or a DLC file.</p>
|
|
435
|
+
<p>
|
|
436
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
437
|
+
</p>''')
|
|
438
|
+
|
|
439
|
+
# Download the package
|
|
440
|
+
if links:
|
|
441
|
+
downloaded = shared_state.download_package(links, title, password, package_id)
|
|
442
|
+
if downloaded:
|
|
443
|
+
StatsHelper(shared_state).increment_package_with_links(links)
|
|
444
|
+
StatsHelper(shared_state).increment_captcha_decryptions_manual()
|
|
445
|
+
shared_state.get_db("protected").delete(package_id)
|
|
446
|
+
|
|
447
|
+
# Check if there are more CAPTCHAs to solve
|
|
448
|
+
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
449
|
+
has_more_captchas = bool(remaining_protected)
|
|
450
|
+
|
|
451
|
+
if has_more_captchas:
|
|
452
|
+
solve_button = render_button("Solve another CAPTCHA", "primary", {
|
|
453
|
+
"onclick": "location.href='/captcha'",
|
|
454
|
+
})
|
|
455
|
+
else:
|
|
456
|
+
solve_button = "<b>No more CAPTCHAs</b>"
|
|
457
|
+
|
|
458
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
459
|
+
<p><b>Success!</b> Package "{title}" bypassed and submitted to JDownloader.</p>
|
|
460
|
+
<p>{len(links)} link(s) processed.</p>
|
|
461
|
+
<p>
|
|
462
|
+
{solve_button}
|
|
463
|
+
</p>
|
|
464
|
+
<p>
|
|
465
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
466
|
+
</p>''')
|
|
467
|
+
else:
|
|
468
|
+
StatsHelper(shared_state).increment_failed_decryptions_manual()
|
|
469
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
470
|
+
<p><b>Error:</b> Failed to submit package to JDownloader.</p>
|
|
471
|
+
<p>
|
|
472
|
+
{render_button("Try Again", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
473
|
+
</p>''')
|
|
474
|
+
else:
|
|
475
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
476
|
+
<p><b>Error:</b> No valid links found.</p>
|
|
477
|
+
<p>
|
|
478
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
479
|
+
</p>''')
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
info(f"Bypass submission error: {e}")
|
|
483
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
484
|
+
<p><b>Error:</b> {str(e)}</p>
|
|
485
|
+
<p>
|
|
486
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
487
|
+
</p>''')
|
|
488
|
+
|
|
349
489
|
@app.post('/captcha/decrypt-filecrypt')
|
|
350
490
|
def submit_token():
|
|
351
491
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
@@ -382,7 +522,8 @@ def setup_captcha_routes(app):
|
|
|
382
522
|
"size_mb": 0,
|
|
383
523
|
"password": password,
|
|
384
524
|
"mirror": mirror,
|
|
385
|
-
"session": session
|
|
525
|
+
"session": session,
|
|
526
|
+
"original_url": link
|
|
386
527
|
})
|
|
387
528
|
shared_state.get_db("protected").update_store(package_id, blob)
|
|
388
529
|
info(f"Another CAPTCHA solution is required for {mirror} link: {replace_url}")
|
|
@@ -432,20 +573,26 @@ def setup_captcha_routes(app):
|
|
|
432
573
|
package_id = payload.get("package_id")
|
|
433
574
|
session_id = payload.get("session")
|
|
434
575
|
title = payload.get("title", "Unknown Package")
|
|
576
|
+
password = payload.get("password", "")
|
|
577
|
+
original_url = payload.get("original_url", "")
|
|
435
578
|
url = payload.get("links")[0] if payload.get("links") else None
|
|
436
579
|
|
|
437
580
|
if not url or not session_id or not package_id:
|
|
438
581
|
response.status = 400
|
|
439
582
|
return "Missing required parameters"
|
|
440
583
|
|
|
584
|
+
# Add bypass section
|
|
585
|
+
bypass_section = render_bypass_section(original_url, package_id, title, password)
|
|
586
|
+
|
|
441
587
|
return render_centered_html(f"""
|
|
442
588
|
<!DOCTYPE html>
|
|
443
589
|
<html>
|
|
444
590
|
<body>
|
|
445
591
|
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
446
592
|
<p><b>Package:</b> {title}</p>
|
|
447
|
-
<
|
|
448
|
-
|
|
593
|
+
<h3>Solve CAPTCHA</h3>
|
|
594
|
+
<form action="/captcha/decrypt-filecrypt-circle?url={url}&session_id={session_id}&package_id={package_id}" method="post">
|
|
595
|
+
<input type="image" src="/captcha/circle.php?url={url}&session_id={session_id}" name="button" alt="Circle CAPTCHA">
|
|
449
596
|
</form>
|
|
450
597
|
<p>
|
|
451
598
|
{render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
|
|
@@ -453,6 +600,7 @@ def setup_captcha_routes(app):
|
|
|
453
600
|
<p>
|
|
454
601
|
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
455
602
|
</p>
|
|
603
|
+
{bypass_section}
|
|
456
604
|
</body>
|
|
457
605
|
</html>""")
|
|
458
606
|
|
|
@@ -20,27 +20,34 @@ from quasarr.providers.log import info, debug
|
|
|
20
20
|
|
|
21
21
|
class CNL:
|
|
22
22
|
def __init__(self, crypted_data):
|
|
23
|
+
debug("Initializing CNL with crypted_data.")
|
|
23
24
|
self.crypted_data = crypted_data
|
|
24
25
|
|
|
25
26
|
def jk_eval(self, f_def):
|
|
27
|
+
debug("Evaluating JavaScript key function.")
|
|
26
28
|
js_code = f"""
|
|
27
29
|
{f_def}
|
|
28
30
|
f();
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
33
|
result = dukpy.evaljs(js_code).strip()
|
|
32
|
-
|
|
34
|
+
debug("JavaScript evaluation complete.")
|
|
33
35
|
return result
|
|
34
36
|
|
|
35
37
|
def aes_decrypt(self, data, key):
|
|
38
|
+
debug("Starting AES decrypt.")
|
|
36
39
|
try:
|
|
37
40
|
encrypted_data = base64.b64decode(data)
|
|
41
|
+
debug("Base64 decode for AES decrypt successful.")
|
|
38
42
|
except Exception as e:
|
|
43
|
+
debug("Base64 decode for AES decrypt failed.")
|
|
39
44
|
raise ValueError("Failed to decode base64 data") from e
|
|
40
45
|
|
|
41
46
|
try:
|
|
42
47
|
key_bytes = bytes.fromhex(key)
|
|
48
|
+
debug("Key successfully converted from hex.")
|
|
43
49
|
except Exception as e:
|
|
50
|
+
debug("Failed converting key from hex.")
|
|
44
51
|
raise ValueError("Failed to convert key to bytes") from e
|
|
45
52
|
|
|
46
53
|
iv = key_bytes
|
|
@@ -48,26 +55,33 @@ class CNL:
|
|
|
48
55
|
|
|
49
56
|
try:
|
|
50
57
|
decrypted_data = cipher.decrypt(encrypted_data)
|
|
58
|
+
debug("AES decrypt operation successful.")
|
|
51
59
|
except ValueError as e:
|
|
60
|
+
debug("AES decrypt operation failed.")
|
|
52
61
|
raise ValueError("Decryption failed") from e
|
|
53
62
|
|
|
54
63
|
try:
|
|
55
|
-
|
|
64
|
+
decoded = decrypted_data.decode('utf-8').replace('\x00', '').replace('\x08', '')
|
|
65
|
+
debug("Decoded AES output successfully.")
|
|
66
|
+
return decoded
|
|
56
67
|
except UnicodeDecodeError as e:
|
|
68
|
+
debug("Failed decoding decrypted AES output.")
|
|
57
69
|
raise ValueError("Failed to decode decrypted data") from e
|
|
58
70
|
|
|
59
71
|
def decrypt(self):
|
|
72
|
+
debug("Starting Click'N'Load decrypt sequence.")
|
|
60
73
|
crypted = self.crypted_data[2]
|
|
61
74
|
jk = "function f(){ return \'" + self.crypted_data[1] + "';}"
|
|
62
75
|
key = self.jk_eval(jk)
|
|
63
76
|
uncrypted = self.aes_decrypt(crypted, key)
|
|
64
77
|
urls = [result for result in uncrypted.split("\r\n") if len(result) > 0]
|
|
65
|
-
|
|
78
|
+
debug(f"Extracted {len(urls)} URLs from CNL decrypt.")
|
|
66
79
|
return urls
|
|
67
80
|
|
|
68
81
|
|
|
69
82
|
class DLC:
|
|
70
83
|
def __init__(self, shared_state, dlc_file):
|
|
84
|
+
debug("Initializing DLC decrypt handler.")
|
|
71
85
|
self.shared_state = shared_state
|
|
72
86
|
self.data = dlc_file
|
|
73
87
|
self.KEY = b"cb99b5cbc24db398"
|
|
@@ -75,6 +89,7 @@ class DLC:
|
|
|
75
89
|
self.API_URL = "http://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=pylo&data="
|
|
76
90
|
|
|
77
91
|
def parse_packages(self, start_node):
|
|
92
|
+
debug("Parsing DLC packages from XML.")
|
|
78
93
|
return [
|
|
79
94
|
(
|
|
80
95
|
base64.b64decode(node.getAttribute("name")).decode("utf-8"),
|
|
@@ -84,41 +99,51 @@ class DLC:
|
|
|
84
99
|
]
|
|
85
100
|
|
|
86
101
|
def parse_links(self, start_node):
|
|
102
|
+
debug("Parsing DLC links in package.")
|
|
87
103
|
return [
|
|
88
104
|
base64.b64decode(node.getElementsByTagName("url")[0].firstChild.data).decode("utf-8")
|
|
89
105
|
for node in start_node.getElementsByTagName("file")
|
|
90
106
|
]
|
|
91
107
|
|
|
92
108
|
def decrypt(self):
|
|
109
|
+
debug("Starting DLC decrypt flow.")
|
|
93
110
|
if not isinstance(self.data, bytes):
|
|
111
|
+
debug("DLC data type invalid.")
|
|
94
112
|
raise TypeError("data must be bytes.")
|
|
95
113
|
|
|
96
114
|
all_urls = []
|
|
97
115
|
|
|
98
116
|
try:
|
|
117
|
+
debug("Preparing DLC data buffer.")
|
|
99
118
|
data = self.data.strip()
|
|
100
|
-
|
|
101
119
|
data += b"=" * (-len(data) % 4)
|
|
102
120
|
|
|
103
121
|
dlc_key = data[-88:].decode("utf-8")
|
|
104
122
|
dlc_data = base64.b64decode(data[:-88])
|
|
123
|
+
debug("DLC base64 decode successful.")
|
|
105
124
|
|
|
106
125
|
headers = {'User-Agent': self.shared_state.values["user_agent"]}
|
|
107
126
|
|
|
127
|
+
debug("Requesting DLC decryption service.")
|
|
108
128
|
dlc_content = requests.get(self.API_URL + dlc_key, headers=headers, timeout=10).content.decode("utf-8")
|
|
109
129
|
|
|
110
130
|
rc = base64.b64decode(re.search(r"<rc>(.+)</rc>", dlc_content, re.S).group(1))[:16]
|
|
131
|
+
debug("Received DLC RC block.")
|
|
111
132
|
|
|
112
133
|
cipher = AES.new(self.KEY, AES.MODE_CBC, self.IV)
|
|
113
134
|
key = iv = cipher.decrypt(rc)
|
|
135
|
+
debug("Decrypted DLC key material.")
|
|
114
136
|
|
|
115
137
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
116
138
|
xml_data = base64.b64decode(cipher.decrypt(dlc_data)).decode("utf-8")
|
|
139
|
+
debug("Final DLC decrypt successful.")
|
|
117
140
|
|
|
118
141
|
root = xml.dom.minidom.parseString(xml_data).documentElement
|
|
119
142
|
content_node = root.getElementsByTagName("content")[0]
|
|
143
|
+
debug("Parsed DLC XML content.")
|
|
120
144
|
|
|
121
145
|
packages = self.parse_packages(content_node)
|
|
146
|
+
debug(f"Found {len(packages)} DLC packages.")
|
|
122
147
|
|
|
123
148
|
for package in packages:
|
|
124
149
|
urls = package[1]
|
|
@@ -128,80 +153,83 @@ class DLC:
|
|
|
128
153
|
info("DLC Error: " + str(e))
|
|
129
154
|
return None
|
|
130
155
|
|
|
156
|
+
debug(f"DLC decrypt yielded {len(all_urls)} URLs.")
|
|
131
157
|
return all_urls
|
|
132
158
|
|
|
133
159
|
|
|
134
160
|
def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=None):
|
|
135
|
-
"""
|
|
136
|
-
Robust Filecrypt fetch:
|
|
137
|
-
- Always check & bypass Cloudflare with FlareSolverr when necessary.
|
|
138
|
-
- Detect password input more reliably.
|
|
139
|
-
- Use session consistently and update user-agent cleanly.
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
161
|
info("Attempting to decrypt Filecrypt link: " + url)
|
|
162
|
+
debug("Initializing Filecrypt session & headers.")
|
|
143
163
|
session = requests.Session()
|
|
144
164
|
headers = {'User-Agent': shared_state.values["user_agent"]}
|
|
145
165
|
|
|
146
|
-
|
|
166
|
+
debug("Ensuring Cloudflare bypass is ready.")
|
|
147
167
|
session, headers, output = ensure_session_cf_bypassed(info, shared_state, session, url, headers)
|
|
148
168
|
if not session or not output:
|
|
169
|
+
debug("Cloudflare bypass failed.")
|
|
149
170
|
return False
|
|
150
171
|
|
|
151
172
|
soup = BeautifulSoup(output.text, 'html.parser')
|
|
173
|
+
debug("Parsed initial Filecrypt HTML.")
|
|
152
174
|
|
|
153
175
|
password_field = None
|
|
154
176
|
try:
|
|
155
|
-
|
|
177
|
+
debug("Attempting password field auto-detection.")
|
|
156
178
|
input_elem = soup.find('input', attrs={'type': 'password'})
|
|
157
179
|
if not input_elem:
|
|
158
180
|
input_elem = soup.find('input', placeholder=lambda v: v and 'password' in v.lower())
|
|
159
181
|
if not input_elem:
|
|
160
|
-
# fallback: name contains 'pass' or 'password'
|
|
161
182
|
input_elem = soup.find('input',
|
|
162
183
|
attrs={'name': lambda v: v and ('pass' in v.lower() or 'password' in v.lower())})
|
|
163
184
|
if input_elem and input_elem.has_attr('name'):
|
|
164
185
|
password_field = input_elem['name']
|
|
165
186
|
info("Password field name identified: " + password_field)
|
|
187
|
+
debug(f"Password field detected: {password_field}")
|
|
166
188
|
except Exception as e:
|
|
167
|
-
# narrow catch so real errors bubble up elsewhere
|
|
168
189
|
info(f"Password-field detection error: {e}")
|
|
190
|
+
debug("Password-field detection error raised.")
|
|
169
191
|
|
|
170
|
-
# If we have a password to submit and a field to submit to, post it.
|
|
171
192
|
if password and password_field:
|
|
172
193
|
info("Using Password: " + password)
|
|
194
|
+
debug("Submitting password via POST.")
|
|
173
195
|
post_headers = {'User-Agent': shared_state.values["user_agent"],
|
|
174
196
|
'Content-Type': 'application/x-www-form-urlencoded'}
|
|
175
197
|
data = {password_field: password}
|
|
176
198
|
try:
|
|
177
199
|
output = session.post(output.url, data=data, headers=post_headers, timeout=30)
|
|
200
|
+
debug("Password POST request successful.")
|
|
178
201
|
except requests.RequestException as e:
|
|
179
202
|
info(f"POSTing password failed: {e}")
|
|
203
|
+
debug("Password POST request failed.")
|
|
180
204
|
return False
|
|
181
205
|
|
|
182
|
-
# After posting, Cloudflare could reappear; ensure still bypassed
|
|
183
206
|
if output.status_code == 403 or is_cloudflare_challenge(output.text):
|
|
184
207
|
info("Encountered Cloudflare after password POST. Re-running FlareSolverr...")
|
|
208
|
+
debug("Cloudflare reappeared after password submit, retrying bypass.")
|
|
185
209
|
session, headers, output = ensure_session_cf_bypassed(info, shared_state, session, output.url, headers)
|
|
186
210
|
if not session or not output:
|
|
211
|
+
debug("Cloudflare bypass failed after password POST.")
|
|
187
212
|
return False
|
|
188
213
|
|
|
189
|
-
else:
|
|
190
|
-
pass
|
|
191
|
-
|
|
192
214
|
url = output.url
|
|
193
215
|
soup = BeautifulSoup(output.text, 'html.parser')
|
|
216
|
+
debug("Re-parsed HTML after password submit or initial load.")
|
|
217
|
+
|
|
194
218
|
if bool(soup.find_all("input", {"id": "p4assw0rt"})):
|
|
195
219
|
info(f"Password was wrong or missing. Could not get links for {title}")
|
|
220
|
+
debug("Incorrect password detected via p4assw0rt.")
|
|
196
221
|
return False
|
|
197
222
|
|
|
198
223
|
no_captcha_present = bool(soup.find("form", {"class": "cnlform"}))
|
|
199
224
|
if no_captcha_present:
|
|
200
225
|
info("No CAPTCHA present. Skipping token!")
|
|
226
|
+
debug("Detected no CAPTCHA (CNL direct form).")
|
|
201
227
|
else:
|
|
202
228
|
circle_captcha = bool(soup.find_all("div", {"class": "circle_captcha"}))
|
|
229
|
+
debug(f"Circle captcha present: {circle_captcha}")
|
|
203
230
|
i = 0
|
|
204
231
|
while circle_captcha and i < 3:
|
|
232
|
+
debug(f"Submitting fake circle captcha click attempt {i+1}.")
|
|
205
233
|
random_x = str(random.randint(100, 200))
|
|
206
234
|
random_y = str(random.randint(100, 200))
|
|
207
235
|
output = session.post(url, data="buttonx.x=" + random_x + "&buttonx.y=" + random_y,
|
|
@@ -210,40 +238,56 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
210
238
|
url = output.url
|
|
211
239
|
soup = BeautifulSoup(output.text, 'html.parser')
|
|
212
240
|
circle_captcha = bool(soup.find_all("div", {"class": "circle_captcha"}))
|
|
241
|
+
i += 1
|
|
242
|
+
debug(f"Circle captcha still present: {circle_captcha}")
|
|
213
243
|
|
|
244
|
+
debug("Submitting final CAPTCHA token.")
|
|
214
245
|
output = session.post(url, data="cap_token=" + token, headers={'User-Agent': shared_state.values["user_agent"],
|
|
215
246
|
'Content-Type': 'application/x-www-form-urlencoded'})
|
|
216
247
|
url = output.url
|
|
217
248
|
|
|
218
249
|
if "/404.html" in url:
|
|
219
250
|
info("Filecrypt returned 404 - current IP is likely banned or the link is offline.")
|
|
251
|
+
debug("Detected Filecrypt 404 page.")
|
|
220
252
|
|
|
221
253
|
soup = BeautifulSoup(output.text, 'html.parser')
|
|
254
|
+
debug("Parsed post-captcha response HTML.")
|
|
222
255
|
|
|
223
256
|
solved = bool(soup.find_all("div", {"class": "container"}))
|
|
224
257
|
if not solved:
|
|
225
258
|
info("Token rejected by Filecrypt! Try another CAPTCHA to proceed...")
|
|
259
|
+
debug("Token rejected; no 'container' div found.")
|
|
226
260
|
return False
|
|
227
261
|
else:
|
|
262
|
+
debug("CAPTCHA token accepted by Filecrypt.")
|
|
263
|
+
|
|
228
264
|
season_number = ""
|
|
229
265
|
episode_number = ""
|
|
230
266
|
episode_in_title = re.findall(r'.*\.s(\d{1,3})e(\d{1,3})\..*', title, re.IGNORECASE)
|
|
231
267
|
season_in_title = re.findall(r'.*\.s(\d{1,3})\..*', title, re.IGNORECASE)
|
|
268
|
+
debug("Attempting episode/season number parsing from title.")
|
|
269
|
+
|
|
232
270
|
if episode_in_title:
|
|
233
271
|
try:
|
|
234
272
|
season_number = str(int(episode_in_title[0][0]))
|
|
235
273
|
episode_number = str(int(episode_in_title[0][1]))
|
|
274
|
+
debug(f"Detected S{season_number}E{episode_number} from title.")
|
|
236
275
|
except:
|
|
276
|
+
debug("Failed parsing S/E numbers from title.")
|
|
237
277
|
pass
|
|
238
278
|
elif season_in_title:
|
|
239
279
|
try:
|
|
240
280
|
season_number = str(int(season_in_title[0]))
|
|
281
|
+
debug(f"Detected season {season_number} from title.")
|
|
241
282
|
except:
|
|
283
|
+
debug("Failed parsing season number from title.")
|
|
242
284
|
pass
|
|
243
285
|
|
|
244
286
|
season = ""
|
|
245
287
|
episode = ""
|
|
246
288
|
tv_show_selector = soup.find("div", {"class": "dlpart"})
|
|
289
|
+
debug(f"TV show selector found: {bool(tv_show_selector)}")
|
|
290
|
+
|
|
247
291
|
if tv_show_selector:
|
|
248
292
|
|
|
249
293
|
season = "season="
|
|
@@ -253,41 +297,53 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
253
297
|
try:
|
|
254
298
|
if season_selection:
|
|
255
299
|
season += str(season_number)
|
|
300
|
+
debug(f"Assigned season parameter: {season}")
|
|
256
301
|
except:
|
|
302
|
+
debug("Failed assigning season parameter.")
|
|
257
303
|
pass
|
|
258
304
|
|
|
259
305
|
episode_selection = soup.find("div", {"id": "selbox_episode"})
|
|
260
306
|
try:
|
|
261
307
|
if episode_selection:
|
|
262
308
|
episode += str(episode_number)
|
|
309
|
+
debug(f"Assigned episode parameter: {episode}")
|
|
263
310
|
except:
|
|
311
|
+
debug("Failed assigning episode parameter.")
|
|
264
312
|
pass
|
|
265
313
|
|
|
266
314
|
if episode_number and not episode:
|
|
267
315
|
info(f"Missing select for episode number {episode_number}! Expect undesired links in the output.")
|
|
316
|
+
debug("Episode number present but no episode selector container found.")
|
|
268
317
|
|
|
269
318
|
links = []
|
|
270
319
|
|
|
271
320
|
mirrors = []
|
|
272
321
|
mirrors_available = soup.select("a[href*=mirror]")
|
|
322
|
+
debug(f"Mirrors available: {len(mirrors_available)}")
|
|
323
|
+
|
|
273
324
|
if not mirror and mirrors_available:
|
|
274
325
|
for mirror in mirrors_available:
|
|
275
326
|
try:
|
|
276
327
|
mirror_query = mirror.get("href").split("?")[1]
|
|
277
328
|
base_url = url.split("?")[0] if "mirror" in url else url
|
|
278
329
|
mirrors.append(f"{base_url}?{mirror_query}")
|
|
330
|
+
debug(f"Discovered mirror: {mirrors[-1]}")
|
|
279
331
|
except IndexError:
|
|
332
|
+
debug("Mirror parsing failed due to missing '?'.")
|
|
280
333
|
continue
|
|
281
334
|
else:
|
|
282
335
|
mirrors = [url]
|
|
336
|
+
debug("Using direct URL as only mirror.")
|
|
283
337
|
|
|
284
338
|
for mirror in mirrors:
|
|
285
339
|
if not len(mirrors) == 1:
|
|
340
|
+
debug(f"Loading mirror: {mirror}")
|
|
286
341
|
output = session.get(mirror, headers=headers)
|
|
287
342
|
url = output.url
|
|
288
343
|
soup = BeautifulSoup(output.text, 'html.parser')
|
|
289
344
|
|
|
290
345
|
try:
|
|
346
|
+
debug("Attempting Click'n'Load decrypt.")
|
|
291
347
|
crypted_payload = soup.find("form", {"class": "cnlform"}).get('onsubmit')
|
|
292
348
|
crypted_data = re.findall(r"'(.*?)'", crypted_payload)
|
|
293
349
|
if not title:
|
|
@@ -298,7 +354,9 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
298
354
|
crypted_data[2],
|
|
299
355
|
title
|
|
300
356
|
]
|
|
357
|
+
|
|
301
358
|
if episode and season:
|
|
359
|
+
debug("Applying episode/season filtering to CNL.")
|
|
302
360
|
domain = urlparse(url).netloc
|
|
303
361
|
filtered_cnl_secret = soup.find("input", {"name": "hidden_cnl_id"}).attrs["value"]
|
|
304
362
|
filtered_cnl_link = f"https://{domain}/_CNL/{filtered_cnl_secret}.html?{season}&{episode}"
|
|
@@ -307,6 +365,7 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
307
365
|
if filtered_cnl_result.status_code == 200:
|
|
308
366
|
filtered_cnl_data = json.loads(filtered_cnl_result.text)
|
|
309
367
|
if filtered_cnl_data["success"]:
|
|
368
|
+
debug("Season/Episode filter applied successfully.")
|
|
310
369
|
crypted_data = [
|
|
311
370
|
crypted_data[0],
|
|
312
371
|
filtered_cnl_data["data"][0],
|
|
@@ -315,12 +374,15 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
315
374
|
]
|
|
316
375
|
links.extend(CNL(crypted_data).decrypt())
|
|
317
376
|
except:
|
|
377
|
+
debug("CNL decrypt failed; trying DLC fallback.")
|
|
318
378
|
if "The owner of this folder has deactivated all hosts in this container in their settings." in soup.text:
|
|
319
379
|
info(f"Mirror deactivated by the owner: {mirror}")
|
|
380
|
+
debug("Mirror deactivated detected in page text.")
|
|
320
381
|
continue
|
|
321
382
|
|
|
322
383
|
info("Click'n'Load not found! Falling back to DLC...")
|
|
323
384
|
try:
|
|
385
|
+
debug("Attempting DLC fallback.")
|
|
324
386
|
crypted_payload = soup.find("button", {"class": "dlcdownload"}).get("onclick")
|
|
325
387
|
crypted_data = re.findall(r"'(.*?)'", crypted_payload)
|
|
326
388
|
dlc_secret = crypted_data[0]
|
|
@@ -332,18 +394,20 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
332
394
|
dlc_file = session.get(dlc_link, headers=headers).content
|
|
333
395
|
links.extend(DLC(shared_state, dlc_file).decrypt())
|
|
334
396
|
except:
|
|
397
|
+
debug("DLC fallback failed, trying button fallback.")
|
|
335
398
|
info("DLC not found! Falling back to first available download Button...")
|
|
336
399
|
|
|
337
400
|
base_url = urlparse(url).netloc
|
|
338
401
|
phpsessid = session.cookies.get('PHPSESSID')
|
|
339
402
|
if not phpsessid:
|
|
340
403
|
info("PHPSESSID cookie not found! Cannot proceed with download links extraction.")
|
|
404
|
+
debug("Missing PHPSESSID cookie.")
|
|
341
405
|
return False
|
|
342
406
|
|
|
343
407
|
results = []
|
|
408
|
+
debug("Parsing fallback buttons for download links.")
|
|
344
409
|
|
|
345
410
|
for button in soup.find_all('button'):
|
|
346
|
-
# Find the correct data-* attribute (only one expected)
|
|
347
411
|
data_attrs = [v for k, v in button.attrs.items() if k.startswith('data-') and k != 'data-i18n']
|
|
348
412
|
if not data_attrs:
|
|
349
413
|
continue
|
|
@@ -356,6 +420,7 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
356
420
|
results.append((full_url, mirror_name))
|
|
357
421
|
|
|
358
422
|
sorted_results = sorted(results, key=lambda x: 0 if 'rapidgator' in x[1].lower() else 1)
|
|
423
|
+
debug(f"Found {len(sorted_results)} fallback link candidates.")
|
|
359
424
|
|
|
360
425
|
for result_url, mirror in sorted_results:
|
|
361
426
|
info("You must solve circlecaptcha separately!")
|
|
@@ -369,8 +434,10 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
|
|
|
369
434
|
|
|
370
435
|
if not links:
|
|
371
436
|
info("No links found in Filecrypt response!")
|
|
437
|
+
debug("Extraction completed but yielded no links.")
|
|
372
438
|
return False
|
|
373
439
|
|
|
440
|
+
debug(f"Returning success with {len(links)} extracted links.")
|
|
374
441
|
return {
|
|
375
442
|
"status": "success",
|
|
376
443
|
"links": links
|
quasarr/providers/version.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
quasarr/__init__.py,sha256=_WoDFvqXXilQynsiPrY-SXyADy1OwhAjQkdaJFqqHo0,17873
|
|
2
2
|
quasarr/api/__init__.py,sha256=9Y_DTNYsHeimrXL3mAli8OUg0zqo7QGLF2ft40d3R-c,6822
|
|
3
3
|
quasarr/api/arr/__init__.py,sha256=HrzyavxsCmQkdV2SMqQSoJq3KgrsPBnQJdo5iyovmG8,16626
|
|
4
|
-
quasarr/api/captcha/__init__.py,sha256=
|
|
4
|
+
quasarr/api/captcha/__init__.py,sha256=EGXuhsks87w6gbj6RbupXuxhOMD3G9dl4zwwJ_U_8ck,32982
|
|
5
5
|
quasarr/api/config/__init__.py,sha256=0K7zqC9dt39Ul1RIJt0zNVdh1b9ARnfC6QFPa2D9FCw,819
|
|
6
6
|
quasarr/api/sponsors_helper/__init__.py,sha256=kAZabPlplPYRG6Uw7ZHTk5uypualwvhs-NoTOjQhhhA,6369
|
|
7
7
|
quasarr/api/statistics/__init__.py,sha256=NrBAjjHkIUE95HhPUGIfNqh2IqBqJ_zm00S90Y-Qnus,7038
|
|
8
8
|
quasarr/downloads/__init__.py,sha256=rRAkr0Kpk30HOvZ2AFt1ujiVPV_-qf7MSN4ytsHb9jE,10488
|
|
9
9
|
quasarr/downloads/linkcrypters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
quasarr/downloads/linkcrypters/al.py,sha256=pM3NDan8x0WU8OS1GV3HuuV4B6Nm0a-ATrVORvLHt9M,8487
|
|
11
|
-
quasarr/downloads/linkcrypters/filecrypt.py,sha256=
|
|
11
|
+
quasarr/downloads/linkcrypters/filecrypt.py,sha256=GT51x_MG_hW4IpOF6OvL5r-2mTnMijI8K7_1D5Bfn4U,18884
|
|
12
12
|
quasarr/downloads/linkcrypters/hide.py,sha256=kMxjsYZJpC1V3jwYv9b0h4HKBIectLlgglwOmexvFXs,4107
|
|
13
13
|
quasarr/downloads/packages/__init__.py,sha256=C-6b_IgKQsmQWo5onTqFqx2pCOvPkT0oH-7jiopa8Hk,16748
|
|
14
14
|
quasarr/downloads/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -33,7 +33,7 @@ quasarr/providers/notifications.py,sha256=bohT-6yudmFnmZMc3BwCGX0n1HdzSVgQG_LDZm
|
|
|
33
33
|
quasarr/providers/obfuscated.py,sha256=XfiEblJizqixUoA4MIsillr5Nh1dwFqCgLvBQWM7Lwo,193865
|
|
34
34
|
quasarr/providers/shared_state.py,sha256=4nswf5AuA4c1DWqSXsX0HXwlDt5e-UUUvQSy-vryCRE,28987
|
|
35
35
|
quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
|
|
36
|
-
quasarr/providers/version.py,sha256=
|
|
36
|
+
quasarr/providers/version.py,sha256=G0ZVMRC6n_iv9xlSiAzj6jfWvXrCkN6rfF__BOiB5lw,4004
|
|
37
37
|
quasarr/providers/web_server.py,sha256=XPj98T-axxgotovuB-rVw1IPCkJiNdXBlEeFvM_zSlM,1432
|
|
38
38
|
quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
quasarr/providers/sessions/al.py,sha256=mlP6SWfCY2HyOSV40uyotQ5T4eSBNYG9A5GWOEAdz-c,9589
|
|
@@ -56,9 +56,9 @@ quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
56
56
|
quasarr/storage/config.py,sha256=ISZXh9gHiBu5mYhHGYx8nZ8JyMYuSFqfVl52DiUDJec,5994
|
|
57
57
|
quasarr/storage/setup.py,sha256=gpHOsc5qtt-M72saZoMJFLE2YlCrjv7FWZknh-iVKsk,17766
|
|
58
58
|
quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
|
|
59
|
-
quasarr-1.
|
|
60
|
-
quasarr-1.
|
|
61
|
-
quasarr-1.
|
|
62
|
-
quasarr-1.
|
|
63
|
-
quasarr-1.
|
|
64
|
-
quasarr-1.
|
|
59
|
+
quasarr-1.17.0.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
|
|
60
|
+
quasarr-1.17.0.dist-info/METADATA,sha256=pXe5L01d2AqOa9kkhW4OhWNsuYyB1ZSplLwqNrnmkRI,12249
|
|
61
|
+
quasarr-1.17.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
62
|
+
quasarr-1.17.0.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
|
|
63
|
+
quasarr-1.17.0.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
|
|
64
|
+
quasarr-1.17.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|