quasarr 1.4.1__py3-none-any.whl → 1.20.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- quasarr/__init__.py +157 -67
- quasarr/api/__init__.py +126 -43
- quasarr/api/arr/__init__.py +197 -78
- quasarr/api/captcha/__init__.py +885 -39
- quasarr/api/config/__init__.py +23 -0
- quasarr/api/sponsors_helper/__init__.py +84 -22
- quasarr/api/statistics/__init__.py +196 -0
- quasarr/downloads/__init__.py +236 -487
- quasarr/downloads/linkcrypters/al.py +237 -0
- quasarr/downloads/linkcrypters/filecrypt.py +178 -31
- quasarr/downloads/linkcrypters/hide.py +123 -0
- quasarr/downloads/packages/__init__.py +461 -0
- quasarr/downloads/sources/al.py +697 -0
- quasarr/downloads/sources/by.py +106 -0
- quasarr/downloads/sources/dd.py +6 -78
- quasarr/downloads/sources/dj.py +7 -0
- quasarr/downloads/sources/dt.py +1 -1
- quasarr/downloads/sources/dw.py +2 -2
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +51 -0
- quasarr/downloads/sources/nx.py +36 -81
- quasarr/downloads/sources/sf.py +27 -4
- quasarr/downloads/sources/sj.py +7 -0
- quasarr/downloads/sources/sl.py +90 -0
- quasarr/downloads/sources/wd.py +110 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +20 -0
- quasarr/providers/html_templates.py +48 -39
- quasarr/providers/imdb_metadata.py +15 -2
- quasarr/providers/myjd_api.py +34 -5
- quasarr/providers/notifications.py +30 -5
- quasarr/providers/obfuscated.py +35 -0
- quasarr/providers/sessions/__init__.py +0 -0
- quasarr/providers/sessions/al.py +286 -0
- quasarr/providers/sessions/dd.py +78 -0
- quasarr/providers/sessions/nx.py +76 -0
- quasarr/providers/shared_state.py +347 -20
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +1 -1
- quasarr/search/__init__.py +112 -36
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +203 -0
- quasarr/search/sources/dd.py +17 -6
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dt.py +37 -7
- quasarr/search/sources/dw.py +27 -47
- quasarr/search/sources/fx.py +27 -29
- quasarr/search/sources/he.py +196 -0
- quasarr/search/sources/mb.py +195 -0
- quasarr/search/sources/nk.py +188 -0
- quasarr/search/sources/nx.py +22 -6
- quasarr/search/sources/sf.py +143 -151
- quasarr/search/sources/sj.py +213 -0
- quasarr/search/sources/sl.py +246 -0
- quasarr/search/sources/wd.py +208 -0
- quasarr/storage/config.py +20 -4
- quasarr/storage/setup.py +216 -51
- quasarr-1.20.4.dist-info/METADATA +304 -0
- quasarr-1.20.4.dist-info/RECORD +72 -0
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
- quasarr/providers/tvmaze_metadata.py +0 -23
- quasarr-1.4.1.dist-info/METADATA +0 -174
- quasarr-1.4.1.dist-info/RECORD +0 -43
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
quasarr/api/captcha/__init__.py
CHANGED
|
@@ -4,26 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
+
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
|
8
|
+
from urllib.parse import quote, unquote, urljoin
|
|
7
9
|
|
|
8
10
|
import requests
|
|
9
|
-
from bottle import request, response
|
|
11
|
+
from bottle import request, response, redirect
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
import quasarr.providers.html_images as images
|
|
14
|
+
from quasarr.downloads.linkcrypters.filecrypt import get_filecrypt_links, DLC
|
|
15
|
+
from quasarr.downloads.packages import delete_package
|
|
12
16
|
from quasarr.providers import shared_state
|
|
13
17
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
14
|
-
from quasarr.providers.log import info
|
|
15
|
-
from quasarr.providers.obfuscated import captcha_js, captcha_values
|
|
18
|
+
from quasarr.providers.log import info, debug
|
|
19
|
+
from quasarr.providers.obfuscated import captcha_js, captcha_values, filecrypt_user_js, junkies_user_js
|
|
20
|
+
from quasarr.providers.statistics import StatsHelper
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def js_single_quoted_string_safe(text):
|
|
24
|
+
return text.replace('\\', '\\\\').replace("'", "\\'")
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
def setup_captcha_routes(app):
|
|
19
28
|
@app.get('/captcha')
|
|
20
|
-
def
|
|
29
|
+
def check_captcha():
|
|
21
30
|
try:
|
|
22
31
|
device = shared_state.values["device"]
|
|
23
32
|
except KeyError:
|
|
24
33
|
device = None
|
|
25
34
|
if not device:
|
|
26
|
-
return render_centered_html(f'''<h1><img src="
|
|
35
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
27
36
|
<p>JDownloader connection not established.</p>
|
|
28
37
|
<p>
|
|
29
38
|
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
@@ -31,7 +40,7 @@ def setup_captcha_routes(app):
|
|
|
31
40
|
|
|
32
41
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
33
42
|
if not protected:
|
|
34
|
-
return render_centered_html(f'''<h1><img src="
|
|
43
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
35
44
|
<p>No protected packages found! CAPTCHA not needed.</p>
|
|
36
45
|
<p>
|
|
37
46
|
{render_button("Confirm", "secondary", {"onclick": "location.href='/'"})}
|
|
@@ -48,9 +57,487 @@ def setup_captcha_routes(app):
|
|
|
48
57
|
except KeyError:
|
|
49
58
|
desired_mirror = None
|
|
50
59
|
|
|
60
|
+
# This is set for circle CAPTCHAs
|
|
61
|
+
filecrypt_session = data.get("session", None)
|
|
62
|
+
|
|
63
|
+
# This is required for cutcaptcha
|
|
64
|
+
rapid = [ln for ln in links if "rapidgator" in ln[1].lower()]
|
|
65
|
+
others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
|
|
66
|
+
prioritized_links = rapid + others
|
|
67
|
+
|
|
68
|
+
# This is required for bypass on circlecaptcha
|
|
69
|
+
original_url = data.get("original_url", "")
|
|
70
|
+
|
|
71
|
+
payload = {
|
|
72
|
+
"package_id": package_id,
|
|
73
|
+
"title": title,
|
|
74
|
+
"password": password,
|
|
75
|
+
"mirror": desired_mirror,
|
|
76
|
+
"session": filecrypt_session,
|
|
77
|
+
"links": prioritized_links,
|
|
78
|
+
"original_url": original_url
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
encoded_payload = urlsafe_b64encode(json.dumps(payload).encode()).decode()
|
|
82
|
+
|
|
83
|
+
sj = shared_state.values["config"]("Hostnames").get("sj")
|
|
84
|
+
dj = shared_state.values["config"]("Hostnames").get("dj")
|
|
85
|
+
has_junkies_links = any(
|
|
86
|
+
(sj and sj in link) or (dj and dj in link)
|
|
87
|
+
for link in prioritized_links
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if has_junkies_links:
|
|
91
|
+
debug("Redirecting to Junkies CAPTCHA")
|
|
92
|
+
redirect(f"/captcha/junkies?data={quote(encoded_payload)}")
|
|
93
|
+
elif filecrypt_session:
|
|
94
|
+
debug(f'Redirecting to circle CAPTCHA')
|
|
95
|
+
redirect(f"/captcha/circle?data={quote(encoded_payload)}")
|
|
96
|
+
else:
|
|
97
|
+
debug(f"Redirecting to cutcaptcha")
|
|
98
|
+
redirect(f"/captcha/cutcaptcha?data={quote(encoded_payload)}")
|
|
99
|
+
|
|
100
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
101
|
+
<p>Unexpected Error!</p>
|
|
102
|
+
<p>
|
|
103
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
104
|
+
</p>''')
|
|
105
|
+
|
|
106
|
+
def decode_payload():
|
|
107
|
+
encoded = request.query.get('data')
|
|
108
|
+
try:
|
|
109
|
+
decoded = urlsafe_b64decode(unquote(encoded)).decode()
|
|
110
|
+
return json.loads(decoded)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return {"error": f"Failed to decode payload: {str(e)}"}
|
|
113
|
+
|
|
114
|
+
@app.get("/captcha/junkies")
|
|
115
|
+
def serve_junkies_captcha():
|
|
116
|
+
payload = decode_payload()
|
|
117
|
+
|
|
118
|
+
if "error" in payload:
|
|
119
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
120
|
+
<p>{payload["error"]}</p>
|
|
121
|
+
<p>
|
|
122
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
123
|
+
</p>''')
|
|
124
|
+
|
|
125
|
+
package_id = payload.get("package_id")
|
|
126
|
+
title = payload.get("title")
|
|
127
|
+
password = payload.get("password")
|
|
128
|
+
urls = payload.get("links")
|
|
129
|
+
url = urls[0]
|
|
130
|
+
|
|
131
|
+
return render_centered_html(f"""
|
|
132
|
+
<!DOCTYPE html>
|
|
133
|
+
<html>
|
|
134
|
+
<body>
|
|
135
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
136
|
+
<p><b>Package:</b> {title}</p>
|
|
137
|
+
{render_junkies_section(url, package_id, title, password)}
|
|
138
|
+
<p>
|
|
139
|
+
{render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
|
|
140
|
+
</p>
|
|
141
|
+
<p>
|
|
142
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
143
|
+
</p>
|
|
144
|
+
|
|
145
|
+
</body>
|
|
146
|
+
</html>""")
|
|
147
|
+
|
|
148
|
+
def render_junkies_section(url, package_id, title, password):
|
|
149
|
+
"""Render the UI section for SJ and DJ pages"""
|
|
150
|
+
|
|
151
|
+
# Generate userscript URL with transfer params
|
|
152
|
+
# Get base URL of current request
|
|
153
|
+
base_url = request.urlparts.scheme + '://' + request.urlparts.netloc
|
|
154
|
+
transfer_url = f"{base_url}/captcha/quick-transfer"
|
|
155
|
+
|
|
156
|
+
url_with_quick_transfer_params = (
|
|
157
|
+
f"{url}?"
|
|
158
|
+
f"transfer_url={quote(transfer_url)}&"
|
|
159
|
+
f"pkg_id={quote(package_id)}&"
|
|
160
|
+
f"pkg_title={quote(title)}&"
|
|
161
|
+
f"pkg_pass={quote(password)}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return f'''
|
|
165
|
+
<div>
|
|
166
|
+
<!-- One-time setup section - visually separated -->
|
|
167
|
+
<div id="setup-instructions" style="background: #2a2a2a; border: 2px solid #444; border-radius: 8px; padding: 16px; margin-bottom: 24px;">
|
|
168
|
+
<h3 style="margin-top: 0; color: #58a6ff;">First Time Setup:</h3>
|
|
169
|
+
<p style="margin-bottom: 8px;">
|
|
170
|
+
<a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. Install Tampermonkey</a>
|
|
171
|
+
</p>
|
|
172
|
+
<p style="margin-top: 0; margin-bottom: 12px;">
|
|
173
|
+
<a href="/captcha/junkies.user.js" target="_blank">2. Install this userscript</a>
|
|
174
|
+
</p>
|
|
175
|
+
<p style="margin-top: 0;">
|
|
176
|
+
<button id="hide-setup-btn" type="button" style="background: #444; color: #fff; border: 1px solid #666; padding: 6px 12px; border-radius: 4px; cursor: pointer;">
|
|
177
|
+
✅ Don't show this again
|
|
178
|
+
</button>
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Hidden "show instructions" link -->
|
|
183
|
+
<div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
|
|
184
|
+
<a href="#" id="show-setup-btn" style="color: #58a6ff;">ℹ️ Show instructions again</a>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<strong><a href="{url_with_quick_transfer_params}" target="_blank">🔗 Obtain the download links here!</a></strong><br><br>
|
|
188
|
+
|
|
189
|
+
<form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
|
|
190
|
+
<input type="hidden" name="package_id" value="{package_id}" />
|
|
191
|
+
<input type="hidden" name="title" value="{title}" />
|
|
192
|
+
<input type="hidden" name="password" value="{password}" />
|
|
193
|
+
|
|
194
|
+
<div>
|
|
195
|
+
<strong>Paste the download links (one per line):</strong>
|
|
196
|
+
<textarea id="links-input" name="links" rows="5" style="width: 100%; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div>
|
|
200
|
+
{render_button("Submit", "primary", {"type": "submit"})}
|
|
201
|
+
</div>
|
|
202
|
+
</form>
|
|
203
|
+
</div>
|
|
204
|
+
<script>
|
|
205
|
+
// Handle setup instructions hide/show
|
|
206
|
+
const hideSetup = localStorage.getItem('hideJunkiesSetupInstructions');
|
|
207
|
+
const setupBox = document.getElementById('setup-instructions');
|
|
208
|
+
const showLink = document.getElementById('show-instructions-link');
|
|
209
|
+
|
|
210
|
+
if (hideSetup === 'true') {{
|
|
211
|
+
setupBox.style.display = 'none';
|
|
212
|
+
showLink.style.display = 'block';
|
|
213
|
+
}}
|
|
214
|
+
|
|
215
|
+
// Hide setup instructions
|
|
216
|
+
document.getElementById('hide-setup-btn').addEventListener('click', function() {{
|
|
217
|
+
localStorage.setItem('hideJunkiesSetupInstructions', 'true');
|
|
218
|
+
setupBox.style.display = 'none';
|
|
219
|
+
showLink.style.display = 'block';
|
|
220
|
+
}});
|
|
221
|
+
|
|
222
|
+
// Show setup instructions again
|
|
223
|
+
document.getElementById('show-setup-btn').addEventListener('click', function(e) {{
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
localStorage.setItem('hideJunkiesSetupInstructions', 'false');
|
|
226
|
+
setupBox.style.display = 'block';
|
|
227
|
+
showLink.style.display = 'none';
|
|
228
|
+
}});
|
|
229
|
+
</script>
|
|
230
|
+
'''
|
|
231
|
+
|
|
232
|
+
@app.get('/captcha/junkies.user.js')
|
|
233
|
+
def serve_junkies_user_js():
|
|
234
|
+
sj = shared_state.values["config"]("Hostnames").get("sj")
|
|
235
|
+
dj = shared_state.values["config"]("Hostnames").get("dj")
|
|
236
|
+
|
|
237
|
+
content = junkies_user_js(sj, dj)
|
|
238
|
+
response.content_type = 'application/javascript'
|
|
239
|
+
return content
|
|
240
|
+
|
|
241
|
+
@app.get('/captcha/filecrypt.user.js')
|
|
242
|
+
def serve_filecrypt_user_js():
|
|
243
|
+
content = filecrypt_user_js()
|
|
244
|
+
response.content_type = 'application/javascript'
|
|
245
|
+
return content
|
|
246
|
+
|
|
247
|
+
def render_filecrypt_bypass_section(url, package_id, title, password):
|
|
248
|
+
"""Render the bypass UI section for both cutcaptcha and circle captcha pages"""
|
|
249
|
+
|
|
250
|
+
# Generate userscript URL with transfer params
|
|
251
|
+
# Get base URL of current request
|
|
252
|
+
base_url = request.urlparts.scheme + '://' + request.urlparts.netloc
|
|
253
|
+
transfer_url = f"{base_url}/captcha/quick-transfer"
|
|
254
|
+
|
|
255
|
+
url_with_quick_transfer_params = (
|
|
256
|
+
f"{url}?"
|
|
257
|
+
f"transfer_url={quote(transfer_url)}&"
|
|
258
|
+
f"pkg_id={quote(package_id)}&"
|
|
259
|
+
f"pkg_title={quote(title)}&"
|
|
260
|
+
f"pkg_pass={quote(password)}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return f'''
|
|
264
|
+
<div style="margin-top: 40px; padding-top: 20px; border-top: 2px solid #ccc;">
|
|
265
|
+
<details id="bypassDetails">
|
|
266
|
+
<summary id="bypassSummary">Show CAPTCHA Bypass</summary><br>
|
|
267
|
+
|
|
268
|
+
<!-- One-time setup section - visually separated -->
|
|
269
|
+
<div id="setup-instructions" style="background: #2a2a2a; border: 2px solid #444; border-radius: 8px; padding: 16px; margin-bottom: 24px;">
|
|
270
|
+
<h3 style="margin-top: 0; color: #58a6ff;">First Time Setup:</h3>
|
|
271
|
+
<p style="margin-bottom: 8px;">
|
|
272
|
+
<a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. Install Tampermonkey</a>
|
|
273
|
+
</p>
|
|
274
|
+
<p style="margin-top: 0; margin-bottom: 12px;">
|
|
275
|
+
<a href="/captcha/filecrypt.user.js" target="_blank">2. Install this userscript</a>
|
|
276
|
+
</p>
|
|
277
|
+
<p style="margin-top: 0;">
|
|
278
|
+
<button id="hide-setup-btn" type="button" style="background: #444; color: #fff; border: 1px solid #666; padding: 6px 12px; border-radius: 4px; cursor: pointer;">
|
|
279
|
+
✅ Don't show this again
|
|
280
|
+
</button>
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<!-- Hidden "show instructions" link -->
|
|
285
|
+
<div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
|
|
286
|
+
<a href="#" id="show-setup-btn" style="color: #58a6ff;">ℹ️ Show instructions again</a>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<strong><a href="{url_with_quick_transfer_params}" target="_blank">🔗 Obtain the download links here!</a></strong><br><br>
|
|
290
|
+
|
|
291
|
+
<form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
|
|
292
|
+
<input type="hidden" name="package_id" value="{package_id}" />
|
|
293
|
+
<input type="hidden" name="title" value="{title}" />
|
|
294
|
+
<input type="hidden" name="password" value="{password}" />
|
|
295
|
+
|
|
296
|
+
<div>
|
|
297
|
+
<strong>Paste the download links (one per line):</strong>
|
|
298
|
+
<textarea id="links-input" name="links" rows="5" style="width: 100%; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div>
|
|
302
|
+
<strong>Or upload DLC file:</strong><br>
|
|
303
|
+
<input type="file" id="dlc-file" name="dlc_file" accept=".dlc" />
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div>
|
|
307
|
+
{render_button("Submit", "primary", {"type": "submit"})}
|
|
308
|
+
</div>
|
|
309
|
+
</form>
|
|
310
|
+
</details>
|
|
311
|
+
</div>
|
|
312
|
+
<script>
|
|
313
|
+
// Handle CAPTCHA Bypass toggle
|
|
314
|
+
const bypassDetails = document.getElementById('bypassDetails');
|
|
315
|
+
const bypassSummary = document.getElementById('bypassSummary');
|
|
316
|
+
|
|
317
|
+
if (bypassDetails && bypassSummary) {{
|
|
318
|
+
bypassDetails.addEventListener('toggle', () => {{
|
|
319
|
+
if (bypassDetails.open) {{
|
|
320
|
+
bypassSummary.textContent = 'Hide CAPTCHA Bypass';
|
|
321
|
+
}} else {{
|
|
322
|
+
bypassSummary.textContent = 'Show CAPTCHA Bypass';
|
|
323
|
+
}}
|
|
324
|
+
}});
|
|
325
|
+
}}
|
|
326
|
+
|
|
327
|
+
// Handle setup instructions hide/show
|
|
328
|
+
const hideSetup = localStorage.getItem('hideFileCryptSetupInstructions');
|
|
329
|
+
const setupBox = document.getElementById('setup-instructions');
|
|
330
|
+
const showLink = document.getElementById('show-instructions-link');
|
|
331
|
+
|
|
332
|
+
if (hideSetup === 'true') {{
|
|
333
|
+
setupBox.style.display = 'none';
|
|
334
|
+
showLink.style.display = 'block';
|
|
335
|
+
}}
|
|
336
|
+
|
|
337
|
+
// Hide setup instructions
|
|
338
|
+
document.getElementById('hide-setup-btn').addEventListener('click', function() {{
|
|
339
|
+
localStorage.setItem('hideFileCryptSetupInstructions', 'true');
|
|
340
|
+
setupBox.style.display = 'none';
|
|
341
|
+
showLink.style.display = 'block';
|
|
342
|
+
}});
|
|
343
|
+
|
|
344
|
+
// Show setup instructions again
|
|
345
|
+
document.getElementById('show-setup-btn').addEventListener('click', function(e) {{
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
localStorage.setItem('hideFileCryptSetupInstructions', 'false');
|
|
348
|
+
setupBox.style.display = 'block';
|
|
349
|
+
showLink.style.display = 'none';
|
|
350
|
+
}});
|
|
351
|
+
</script>
|
|
352
|
+
'''
|
|
353
|
+
|
|
354
|
+
@app.get('/captcha/quick-transfer')
|
|
355
|
+
def handle_quick_transfer():
|
|
356
|
+
"""Handle quick transfer from userscript"""
|
|
357
|
+
import zlib
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
package_id = request.query.get('pkg_id')
|
|
361
|
+
compressed_links = request.query.get('links', '')
|
|
362
|
+
|
|
363
|
+
if not package_id or not compressed_links:
|
|
364
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
365
|
+
<p><b>Error:</b> Missing parameters</p>
|
|
366
|
+
<p>
|
|
367
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
368
|
+
</p>''')
|
|
369
|
+
|
|
370
|
+
# Decode the compressed links using urlsafe_b64decode
|
|
371
|
+
# Add padding if needed
|
|
372
|
+
padding = 4 - (len(compressed_links) % 4)
|
|
373
|
+
if padding != 4:
|
|
374
|
+
compressed_links += '=' * padding
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
decoded = urlsafe_b64decode(compressed_links)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
info(f"Base64 decode error: {e}")
|
|
380
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
381
|
+
<p><b>Error:</b> Failed to decode data: {str(e)}</p>
|
|
382
|
+
<p>
|
|
383
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
384
|
+
</p>''')
|
|
385
|
+
|
|
386
|
+
# Decompress using zlib - use raw deflate format (no header)
|
|
387
|
+
try:
|
|
388
|
+
decompressed = zlib.decompress(decoded, -15) # -15 = raw deflate, no zlib header
|
|
389
|
+
except Exception as e:
|
|
390
|
+
info(f"Decompression error: {e}, trying with header...")
|
|
391
|
+
try:
|
|
392
|
+
# Fallback: try with zlib header
|
|
393
|
+
decompressed = zlib.decompress(decoded)
|
|
394
|
+
except Exception as e2:
|
|
395
|
+
info(f"Decompression also failed with header: {e2}")
|
|
396
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
397
|
+
<p><b>Error:</b> Failed to decompress data: {str(e)}</p>
|
|
398
|
+
<p>
|
|
399
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
400
|
+
</p>''')
|
|
401
|
+
|
|
402
|
+
links_text = decompressed.decode('utf-8')
|
|
403
|
+
|
|
404
|
+
# Parse links and restore protocols
|
|
405
|
+
raw_links = [link.strip() for link in links_text.split('\n') if link.strip()]
|
|
406
|
+
links = []
|
|
407
|
+
for link in raw_links:
|
|
408
|
+
if not link.startswith(('http://', 'https://')):
|
|
409
|
+
link = 'https://' + link
|
|
410
|
+
links.append(link)
|
|
411
|
+
|
|
412
|
+
info(f"Quick transfer received {len(links)} links for package {package_id}")
|
|
413
|
+
|
|
414
|
+
# Get package info
|
|
415
|
+
raw_data = shared_state.get_db("protected").retrieve(package_id)
|
|
416
|
+
if not raw_data:
|
|
417
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
418
|
+
<p><b>Error:</b> Package not found</p>
|
|
419
|
+
<p>
|
|
420
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
421
|
+
</p>''')
|
|
422
|
+
|
|
423
|
+
data = json.loads(raw_data)
|
|
424
|
+
title = data.get("title", "Unknown")
|
|
425
|
+
password = data.get("password", "")
|
|
426
|
+
|
|
427
|
+
# Download the package
|
|
428
|
+
downloaded = shared_state.download_package(links, title, password, package_id)
|
|
429
|
+
|
|
430
|
+
if downloaded:
|
|
431
|
+
StatsHelper(shared_state).increment_package_with_links(links)
|
|
432
|
+
StatsHelper(shared_state).increment_captcha_decryptions_manual()
|
|
433
|
+
shared_state.get_db("protected").delete(package_id)
|
|
434
|
+
|
|
435
|
+
info(f"Quick transfer successful: {len(links)} links processed")
|
|
436
|
+
|
|
437
|
+
# Check if more CAPTCHAs remain
|
|
438
|
+
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
439
|
+
has_more_captchas = bool(remaining_protected)
|
|
440
|
+
|
|
441
|
+
if has_more_captchas:
|
|
442
|
+
solve_button = render_button("Solve another CAPTCHA", "primary",
|
|
443
|
+
{"onclick": "location.href='/captcha'"})
|
|
444
|
+
else:
|
|
445
|
+
solve_button = "<b>No more CAPTCHAs</b>"
|
|
446
|
+
|
|
447
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
448
|
+
<p><b>✅ Quick Transfer Successful!</b></p>
|
|
449
|
+
<p>Package "{title}" with {len(links)} link(s) submitted to JDownloader.</p>
|
|
450
|
+
<p>
|
|
451
|
+
{solve_button}
|
|
452
|
+
</p>
|
|
453
|
+
<p>
|
|
454
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
455
|
+
</p>''')
|
|
456
|
+
else:
|
|
457
|
+
StatsHelper(shared_state).increment_failed_decryptions_manual()
|
|
458
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
459
|
+
<p><b>Error:</b> Failed to submit package to JDownloader</p>
|
|
460
|
+
<p>
|
|
461
|
+
{render_button("Try Again", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
462
|
+
</p>''')
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
info(f"Quick transfer error: {e}")
|
|
466
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
467
|
+
<p><b>Error:</b> {str(e)}</p>
|
|
468
|
+
<p>
|
|
469
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
470
|
+
</p>''')
|
|
471
|
+
|
|
472
|
+
@app.get('/captcha/delete/<package_id>')
|
|
473
|
+
def delete_captcha_package(package_id):
|
|
474
|
+
success = delete_package(shared_state, package_id)
|
|
475
|
+
|
|
476
|
+
# Check if there are more CAPTCHAs to solve after deletion
|
|
477
|
+
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
478
|
+
has_more_captchas = bool(remaining_protected)
|
|
479
|
+
|
|
480
|
+
if has_more_captchas:
|
|
481
|
+
solve_button = render_button("Solve another CAPTCHA", "primary", {
|
|
482
|
+
"onclick": "location.href='/captcha'",
|
|
483
|
+
})
|
|
484
|
+
else:
|
|
485
|
+
solve_button = "<b>No more CAPTCHAs</b>"
|
|
486
|
+
|
|
487
|
+
if success:
|
|
488
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
489
|
+
<p>Package successfully deleted!</p>
|
|
490
|
+
<p>
|
|
491
|
+
{solve_button}
|
|
492
|
+
</p>
|
|
493
|
+
<p>
|
|
494
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
495
|
+
</p>''')
|
|
496
|
+
else:
|
|
497
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
498
|
+
<p>Failed to delete package!</p>
|
|
499
|
+
<p>
|
|
500
|
+
{solve_button}
|
|
501
|
+
</p>
|
|
502
|
+
<p>
|
|
503
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
504
|
+
</p>''')
|
|
505
|
+
|
|
506
|
+
# The following routes are for cutcaptcha
|
|
507
|
+
@app.get('/captcha/cutcaptcha')
|
|
508
|
+
def serve_cutcaptcha():
|
|
509
|
+
payload = decode_payload()
|
|
510
|
+
|
|
511
|
+
if "error" in payload:
|
|
512
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
513
|
+
<p>{payload["error"]}</p>
|
|
514
|
+
<p>
|
|
515
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
516
|
+
</p>''')
|
|
517
|
+
|
|
518
|
+
package_id = payload.get("package_id")
|
|
519
|
+
title = payload.get("title")
|
|
520
|
+
password = payload.get("password")
|
|
521
|
+
desired_mirror = payload.get("mirror")
|
|
522
|
+
prioritized_links = payload.get("links")
|
|
523
|
+
|
|
524
|
+
if not prioritized_links:
|
|
525
|
+
# No links found, show an error message
|
|
526
|
+
return render_centered_html(f'''
|
|
527
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
528
|
+
<p style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
|
|
529
|
+
<p><b>Error:</b> No download links available for this package.</p>
|
|
530
|
+
<p>
|
|
531
|
+
{render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
|
|
532
|
+
</p>
|
|
533
|
+
<p>
|
|
534
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
535
|
+
</p>
|
|
536
|
+
''')
|
|
537
|
+
|
|
51
538
|
link_options = ""
|
|
52
|
-
if len(
|
|
53
|
-
for link in
|
|
539
|
+
if len(prioritized_links) > 1:
|
|
540
|
+
for link in prioritized_links:
|
|
54
541
|
if "filecrypt." in link[0]:
|
|
55
542
|
link_options += f'<option value="{link[0]}">{link[1]}</option>'
|
|
56
543
|
link_select = f'''<div id="mirrors-select">
|
|
@@ -67,20 +554,38 @@ def setup_captcha_routes(app):
|
|
|
67
554
|
</script>
|
|
68
555
|
'''
|
|
69
556
|
else:
|
|
70
|
-
link_select = f'<div id="mirrors-select">Mirror: <b>{
|
|
557
|
+
link_select = f'<div id="mirrors-select">Mirror: <b>{prioritized_links[0][1]}</b></div>'
|
|
558
|
+
|
|
559
|
+
# Pre-render button HTML in Python
|
|
560
|
+
solve_another_html = render_button("Solve another CAPTCHA", "primary", {"onclick": "location.href='/captcha'"})
|
|
561
|
+
back_button_html = render_button("Back", "secondary", {"onclick": "location.href='/'"})
|
|
562
|
+
|
|
563
|
+
url = prioritized_links[0][0]
|
|
564
|
+
|
|
565
|
+
# Add bypass section
|
|
566
|
+
bypass_section = render_filecrypt_bypass_section(url, package_id, title, password)
|
|
71
567
|
|
|
72
568
|
content = render_centered_html(r'''
|
|
73
569
|
<script type="text/javascript">
|
|
74
570
|
var api_key = "''' + captcha_values()["api_key"] + r'''";
|
|
75
571
|
var endpoint = '/' + window.location.pathname.split('/')[1] + '/' + api_key + '.html';
|
|
572
|
+
var solveAnotherHtml = `<p>''' + solve_another_html + r'''</p><p>''' + back_button_html + r'''</p>`;
|
|
573
|
+
var noMoreHtml = `<p><b>No more CAPTCHAs</b></p><p>''' + back_button_html + r'''</p>`;
|
|
574
|
+
|
|
76
575
|
function handleToken(token) {
|
|
77
576
|
document.getElementById("puzzle-captcha").remove();
|
|
78
577
|
document.getElementById("mirrors-select").remove();
|
|
578
|
+
document.getElementById("delete-package-section").style.display = "none";
|
|
579
|
+
document.getElementById("back-button-section").style.display = "none";
|
|
580
|
+
document.getElementById("bypass-section").style.display = "none";
|
|
581
|
+
|
|
582
|
+
// Remove width limit on result screen
|
|
583
|
+
var packageTitle = document.getElementById("package-title");
|
|
584
|
+
packageTitle.style.maxWidth = "none";
|
|
585
|
+
|
|
79
586
|
document.getElementById("captcha-key").innerText = 'Using result "' + token + '" to decrypt links...';
|
|
80
587
|
var link = document.getElementById("link-hidden").value;
|
|
81
|
-
const
|
|
82
|
-
const endpoint = '/decrypt-filecrypt';
|
|
83
|
-
const fullPath = currentPath.endsWith('/') ? currentPath + endpoint.slice(1) : currentPath + endpoint;
|
|
588
|
+
const fullPath = '/captcha/decrypt-filecrypt';
|
|
84
589
|
|
|
85
590
|
fetch(fullPath, {
|
|
86
591
|
method: 'POST',
|
|
@@ -90,7 +595,7 @@ def setup_captcha_routes(app):
|
|
|
90
595
|
body: JSON.stringify({
|
|
91
596
|
token: token,
|
|
92
597
|
''' + f'''package_id: '{package_id}',
|
|
93
|
-
title: '{title}',
|
|
598
|
+
title: '{js_single_quoted_string_safe(title)}',
|
|
94
599
|
link: link,
|
|
95
600
|
password: '{password}',
|
|
96
601
|
mirror: '{desired_mirror}',
|
|
@@ -100,35 +605,50 @@ def setup_captcha_routes(app):
|
|
|
100
605
|
.then(data => {
|
|
101
606
|
if (data.success) {
|
|
102
607
|
document.getElementById("captcha-key").insertAdjacentHTML('afterend',
|
|
103
|
-
'<p
|
|
608
|
+
'<p>✅ Successful!</p>');
|
|
104
609
|
} else {
|
|
105
610
|
document.getElementById("captcha-key").insertAdjacentHTML('afterend',
|
|
106
611
|
'<p>Failed. Check console for details!</p>');
|
|
107
612
|
}
|
|
108
|
-
|
|
613
|
+
|
|
614
|
+
// Show appropriate button based on whether more CAPTCHAs exist
|
|
615
|
+
var reloadSection = document.getElementById("reload-button");
|
|
616
|
+
if (data.has_more_captchas) {
|
|
617
|
+
reloadSection.innerHTML = solveAnotherHtml;
|
|
618
|
+
} else {
|
|
619
|
+
reloadSection.innerHTML = noMoreHtml;
|
|
620
|
+
}
|
|
621
|
+
reloadSection.style.display = "block";
|
|
109
622
|
});
|
|
110
623
|
}
|
|
111
624
|
''' + captcha_js() + f'''</script>
|
|
112
625
|
<div>
|
|
113
|
-
<h1><img src="
|
|
626
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
627
|
+
<p id="package-title" style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
|
|
114
628
|
<div id="captcha-key"></div>
|
|
115
629
|
{link_select}<br><br>
|
|
116
|
-
<input type="hidden" id="link-hidden" value="{
|
|
630
|
+
<input type="hidden" id="link-hidden" value="{prioritized_links[0][0]}" />
|
|
117
631
|
<div class="captcha-container">
|
|
118
632
|
<div id="puzzle-captcha" aria-style="mobile">
|
|
119
633
|
<strong>Your adblocker prevents the captcha from loading. Disable it!</strong>
|
|
120
634
|
</div>
|
|
121
635
|
</div>
|
|
122
636
|
<div id="reload-button" style="display: none;">
|
|
123
|
-
|
|
124
|
-
{render_button("Solve another CAPTCHA", "secondary", {
|
|
125
|
-
"onclick": "location.reload()",
|
|
126
|
-
})}</p>
|
|
127
|
-
</div>
|
|
637
|
+
</div>
|
|
128
638
|
<br>
|
|
639
|
+
<div id="delete-package-section">
|
|
640
|
+
<p>
|
|
641
|
+
{render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
|
|
642
|
+
</p>
|
|
643
|
+
</div>
|
|
644
|
+
<div id="back-button-section">
|
|
129
645
|
<p>
|
|
130
646
|
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
131
647
|
</p>
|
|
648
|
+
</div>
|
|
649
|
+
<div id="bypass-section">
|
|
650
|
+
{bypass_section}
|
|
651
|
+
</div>
|
|
132
652
|
</div>
|
|
133
653
|
</html>''')
|
|
134
654
|
|
|
@@ -140,13 +660,17 @@ def setup_captcha_routes(app):
|
|
|
140
660
|
|
|
141
661
|
headers = {key: value for key, value in request.headers.items() if key != 'Host'}
|
|
142
662
|
data = request.body.read()
|
|
143
|
-
resp = requests.post(target_url, headers=headers, data=data)
|
|
663
|
+
resp = requests.post(target_url, headers=headers, data=data, verify=False)
|
|
144
664
|
|
|
145
665
|
response.content_type = resp.headers.get('Content-Type')
|
|
146
666
|
|
|
147
667
|
content = resp.text
|
|
148
|
-
content = re.sub(
|
|
149
|
-
|
|
668
|
+
content = re.sub(
|
|
669
|
+
r'''<script\s+src="/(jquery(?:-ui|\.ui\.touch-punch\.min)?\.js)(?:\?[^"]*)?"></script>''',
|
|
670
|
+
r'''<script src="/captcha/js/\1"></script>''',
|
|
671
|
+
content
|
|
672
|
+
)
|
|
673
|
+
|
|
150
674
|
response.content_type = 'text/html'
|
|
151
675
|
return content
|
|
152
676
|
|
|
@@ -156,17 +680,30 @@ def setup_captcha_routes(app):
|
|
|
156
680
|
|
|
157
681
|
headers = {key: value for key, value in request.headers.items() if key != 'Host'}
|
|
158
682
|
data = request.body.read()
|
|
159
|
-
resp = requests.post(target_url, headers=headers, data=data)
|
|
683
|
+
resp = requests.post(target_url, headers=headers, data=data, verify=False)
|
|
160
684
|
|
|
161
685
|
response.content_type = resp.headers.get('Content-Type')
|
|
162
686
|
return resp.content
|
|
163
687
|
|
|
688
|
+
@app.get('/captcha/js/<filename>')
|
|
689
|
+
def serve_local_js(filename):
|
|
690
|
+
upstream = f"{captcha_values()['url']}/{filename}"
|
|
691
|
+
try:
|
|
692
|
+
upstream_resp = requests.get(upstream, verify=False, stream=True)
|
|
693
|
+
upstream_resp.raise_for_status()
|
|
694
|
+
except requests.RequestException as e:
|
|
695
|
+
response.status = 502
|
|
696
|
+
return f"/* Error proxying {filename}: {e} */"
|
|
697
|
+
|
|
698
|
+
response.content_type = 'application/javascript'
|
|
699
|
+
return upstream_resp.iter_content(chunk_size=8192)
|
|
700
|
+
|
|
164
701
|
@app.get('/captcha/<captcha_id>/<uuid>/<filename>')
|
|
165
702
|
def proxy_pngs(captcha_id, uuid, filename):
|
|
166
703
|
new_url = f"{captcha_values()["url"]}/captcha/{captcha_id}/{uuid}/{filename}"
|
|
167
704
|
|
|
168
705
|
try:
|
|
169
|
-
external_response = requests.get(new_url, stream=True)
|
|
706
|
+
external_response = requests.get(new_url, stream=True, verify=False)
|
|
170
707
|
external_response.raise_for_status()
|
|
171
708
|
response.content_type = 'image/png'
|
|
172
709
|
response.headers['Content-Disposition'] = f'inline; filename="{filename}"'
|
|
@@ -182,7 +719,7 @@ def setup_captcha_routes(app):
|
|
|
182
719
|
headers = {key: value for key, value in request.headers.items()}
|
|
183
720
|
|
|
184
721
|
data = request.body.read()
|
|
185
|
-
resp = requests.post(new_url, headers=headers, data=data)
|
|
722
|
+
resp = requests.post(new_url, headers=headers, data=data, verify=False)
|
|
186
723
|
|
|
187
724
|
response.status = resp.status_code
|
|
188
725
|
for header in resp.headers:
|
|
@@ -190,14 +727,123 @@ def setup_captcha_routes(app):
|
|
|
190
727
|
response.set_header(header, resp.headers[header])
|
|
191
728
|
return resp.content
|
|
192
729
|
|
|
730
|
+
@app.post('/captcha/bypass-submit')
|
|
731
|
+
def handle_bypass_submit():
|
|
732
|
+
"""Handle bypass submission with either links or DLC file"""
|
|
733
|
+
try:
|
|
734
|
+
package_id = request.forms.get('package_id')
|
|
735
|
+
title = request.forms.get('title')
|
|
736
|
+
password = request.forms.get('password', '')
|
|
737
|
+
links_input = request.forms.get('links', '').strip()
|
|
738
|
+
dlc_upload = request.files.get('dlc_file')
|
|
739
|
+
|
|
740
|
+
if not package_id or not title:
|
|
741
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
742
|
+
<p><b>Error:</b> Missing package information.</p>
|
|
743
|
+
<p>
|
|
744
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
745
|
+
</p>''')
|
|
746
|
+
|
|
747
|
+
package_exists = shared_state.get_db("protected").retrieve(package_id)
|
|
748
|
+
if not package_exists:
|
|
749
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
750
|
+
<p><b>Error:</b> Package not found or already solved.</p>
|
|
751
|
+
<p>
|
|
752
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
753
|
+
</p>''')
|
|
754
|
+
|
|
755
|
+
# Process links input
|
|
756
|
+
if links_input:
|
|
757
|
+
info(f"Processing direct links bypass for {title}")
|
|
758
|
+
raw_links = [link.strip() for link in links_input.split('\n') if link.strip()]
|
|
759
|
+
links = [l for l in raw_links if l.lower().startswith(("http://", "https://"))]
|
|
760
|
+
|
|
761
|
+
info(f"Received {len(links)} valid direct download links "
|
|
762
|
+
f"(from {len(raw_links)} provided)")
|
|
763
|
+
|
|
764
|
+
# Process DLC file
|
|
765
|
+
elif dlc_upload:
|
|
766
|
+
info(f"Processing DLC file bypass for {title}")
|
|
767
|
+
dlc_content = dlc_upload.file.read()
|
|
768
|
+
try:
|
|
769
|
+
decrypted_links = DLC(shared_state, dlc_content).decrypt()
|
|
770
|
+
if decrypted_links:
|
|
771
|
+
links = decrypted_links
|
|
772
|
+
info(f"Decrypted {len(links)} links from DLC file")
|
|
773
|
+
else:
|
|
774
|
+
raise ValueError("DLC decryption returned no links")
|
|
775
|
+
except Exception as e:
|
|
776
|
+
info(f"DLC decryption failed: {e}")
|
|
777
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
778
|
+
<p><b>Error:</b> Failed to decrypt DLC file: {str(e)}</p>
|
|
779
|
+
<p>
|
|
780
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
781
|
+
</p>''')
|
|
782
|
+
else:
|
|
783
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
784
|
+
<p><b>Error:</b> Please provide either links or a DLC file.</p>
|
|
785
|
+
<p>
|
|
786
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
787
|
+
</p>''')
|
|
788
|
+
|
|
789
|
+
# Download the package
|
|
790
|
+
if links:
|
|
791
|
+
downloaded = shared_state.download_package(links, title, password, package_id)
|
|
792
|
+
if downloaded:
|
|
793
|
+
StatsHelper(shared_state).increment_package_with_links(links)
|
|
794
|
+
StatsHelper(shared_state).increment_captcha_decryptions_manual()
|
|
795
|
+
shared_state.get_db("protected").delete(package_id)
|
|
796
|
+
|
|
797
|
+
# Check if there are more CAPTCHAs to solve
|
|
798
|
+
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
799
|
+
has_more_captchas = bool(remaining_protected)
|
|
800
|
+
|
|
801
|
+
if has_more_captchas:
|
|
802
|
+
solve_button = render_button("Solve another CAPTCHA", "primary", {
|
|
803
|
+
"onclick": "location.href='/captcha'",
|
|
804
|
+
})
|
|
805
|
+
else:
|
|
806
|
+
solve_button = "<b>No more CAPTCHAs</b>"
|
|
807
|
+
|
|
808
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
809
|
+
<p><b>Success!</b> Package "{title}" bypassed and submitted to JDownloader.</p>
|
|
810
|
+
<p>{len(links)} link(s) processed.</p>
|
|
811
|
+
<p>
|
|
812
|
+
{solve_button}
|
|
813
|
+
</p>
|
|
814
|
+
<p>
|
|
815
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
816
|
+
</p>''')
|
|
817
|
+
else:
|
|
818
|
+
StatsHelper(shared_state).increment_failed_decryptions_manual()
|
|
819
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
820
|
+
<p><b>Error:</b> Failed to submit package to JDownloader.</p>
|
|
821
|
+
<p>
|
|
822
|
+
{render_button("Try Again", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
823
|
+
</p>''')
|
|
824
|
+
else:
|
|
825
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
826
|
+
<p><b>Error:</b> No valid links found.</p>
|
|
827
|
+
<p>
|
|
828
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
829
|
+
</p>''')
|
|
830
|
+
|
|
831
|
+
except Exception as e:
|
|
832
|
+
info(f"Bypass submission error: {e}")
|
|
833
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
834
|
+
<p><b>Error:</b> {str(e)}</p>
|
|
835
|
+
<p>
|
|
836
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
837
|
+
</p>''')
|
|
838
|
+
|
|
193
839
|
@app.post('/captcha/decrypt-filecrypt')
|
|
194
840
|
def submit_token():
|
|
195
841
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
196
842
|
if not protected:
|
|
197
843
|
return {"success": False, "title": "No protected packages found! CAPTCHA not needed."}
|
|
198
844
|
|
|
199
|
-
|
|
200
|
-
|
|
845
|
+
links = []
|
|
846
|
+
title = "Unknown Package"
|
|
201
847
|
try:
|
|
202
848
|
data = request.json
|
|
203
849
|
token = data.get('token')
|
|
@@ -210,20 +856,220 @@ def setup_captcha_routes(app):
|
|
|
210
856
|
if token:
|
|
211
857
|
info(f"Received token: {token}")
|
|
212
858
|
info(f"Decrypting links for {title}")
|
|
213
|
-
|
|
859
|
+
decrypted = get_filecrypt_links(shared_state, token, title, link, password=password, mirror=mirror)
|
|
860
|
+
if decrypted:
|
|
861
|
+
if decrypted.get("status", "") == "replaced":
|
|
862
|
+
replace_url = decrypted.get("replace_url")
|
|
863
|
+
session = decrypted.get("session")
|
|
864
|
+
mirror = decrypted.get("mirror", "filecrypt")
|
|
214
865
|
|
|
215
|
-
|
|
866
|
+
links = [replace_url]
|
|
867
|
+
|
|
868
|
+
blob = json.dumps(
|
|
869
|
+
{
|
|
870
|
+
"title": title,
|
|
871
|
+
"links": [replace_url, mirror],
|
|
872
|
+
"size_mb": 0,
|
|
873
|
+
"password": password,
|
|
874
|
+
"mirror": mirror,
|
|
875
|
+
"session": session,
|
|
876
|
+
"original_url": link
|
|
877
|
+
})
|
|
878
|
+
shared_state.get_db("protected").update_store(package_id, blob)
|
|
879
|
+
info(f"Another CAPTCHA solution is required for {mirror} link: {replace_url}")
|
|
216
880
|
|
|
217
|
-
if download_links:
|
|
218
|
-
downloaded = shared_state.download_package(download_links, title, password, package_id)
|
|
219
|
-
if downloaded:
|
|
220
|
-
shared_state.get_db("protected").delete(package_id)
|
|
221
881
|
else:
|
|
222
|
-
|
|
882
|
+
links = decrypted.get("links", [])
|
|
883
|
+
info(f"Decrypted {len(links)} download links for {title}")
|
|
884
|
+
if not links:
|
|
885
|
+
raise ValueError("No download links found after decryption")
|
|
886
|
+
downloaded = shared_state.download_package(links, title, password, package_id)
|
|
887
|
+
if downloaded:
|
|
888
|
+
StatsHelper(shared_state).increment_package_with_links(links)
|
|
889
|
+
shared_state.get_db("protected").delete(package_id)
|
|
890
|
+
else:
|
|
891
|
+
links = []
|
|
892
|
+
raise RuntimeError("Submitting Download to JDownloader failed")
|
|
223
893
|
else:
|
|
224
894
|
raise ValueError("No download links found")
|
|
225
895
|
|
|
226
896
|
except Exception as e:
|
|
227
897
|
info(f"Error decrypting: {e}")
|
|
228
898
|
|
|
229
|
-
|
|
899
|
+
success = bool(links)
|
|
900
|
+
if success:
|
|
901
|
+
StatsHelper(shared_state).increment_captcha_decryptions_manual()
|
|
902
|
+
else:
|
|
903
|
+
StatsHelper(shared_state).increment_failed_decryptions_manual()
|
|
904
|
+
|
|
905
|
+
# Check if there are more CAPTCHAs to solve
|
|
906
|
+
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
907
|
+
has_more_captchas = bool(remaining_protected)
|
|
908
|
+
|
|
909
|
+
return {"success": success, "title": title, "has_more_captchas": has_more_captchas}
|
|
910
|
+
|
|
911
|
+
# The following routes are for circle CAPTCHA
|
|
912
|
+
@app.get('/captcha/circle')
|
|
913
|
+
def serve_circle():
|
|
914
|
+
payload = decode_payload()
|
|
915
|
+
|
|
916
|
+
if "error" in payload:
|
|
917
|
+
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
918
|
+
<p>{payload["error"]}</p>
|
|
919
|
+
<p>
|
|
920
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
921
|
+
</p>''')
|
|
922
|
+
|
|
923
|
+
package_id = payload.get("package_id")
|
|
924
|
+
session_id = payload.get("session")
|
|
925
|
+
title = payload.get("title", "Unknown Package")
|
|
926
|
+
password = payload.get("password", "")
|
|
927
|
+
original_url = payload.get("original_url", "")
|
|
928
|
+
url = payload.get("links")[0] if payload.get("links") else None
|
|
929
|
+
|
|
930
|
+
if not url or not session_id or not package_id:
|
|
931
|
+
response.status = 400
|
|
932
|
+
return "Missing required parameters"
|
|
933
|
+
|
|
934
|
+
# Add bypass section
|
|
935
|
+
bypass_section = render_filecrypt_bypass_section(original_url, package_id, title, password)
|
|
936
|
+
|
|
937
|
+
return render_centered_html(f"""
|
|
938
|
+
<!DOCTYPE html>
|
|
939
|
+
<html>
|
|
940
|
+
<body>
|
|
941
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
942
|
+
<p><b>Package:</b> {title}</p>
|
|
943
|
+
<form action="/captcha/decrypt-filecrypt-circle?url={url}&session_id={session_id}&package_id={package_id}" method="post">
|
|
944
|
+
<input type="image" src="/captcha/circle.php?url={url}&session_id={session_id}" name="button" alt="Circle CAPTCHA">
|
|
945
|
+
</form>
|
|
946
|
+
<p>
|
|
947
|
+
{render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
|
|
948
|
+
</p>
|
|
949
|
+
<p>
|
|
950
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
951
|
+
</p>
|
|
952
|
+
{bypass_section}
|
|
953
|
+
</body>
|
|
954
|
+
</html>""")
|
|
955
|
+
|
|
956
|
+
@app.get('/captcha/circle.php')
|
|
957
|
+
def proxy_circle_php():
|
|
958
|
+
target_url = "https://filecrypt.cc/captcha/circle.php"
|
|
959
|
+
|
|
960
|
+
url = request.query.get('url')
|
|
961
|
+
session_id = request.query.get('session_id')
|
|
962
|
+
if not url or not session_id:
|
|
963
|
+
response.status = 400
|
|
964
|
+
return "Missing required parameters"
|
|
965
|
+
|
|
966
|
+
headers = {'User-Agent': shared_state.values["user_agent"]}
|
|
967
|
+
cookies = {'PHPSESSID': session_id}
|
|
968
|
+
resp = requests.get(target_url, headers=headers, cookies=cookies, verify=False)
|
|
969
|
+
|
|
970
|
+
response.content_type = resp.headers.get('Content-Type', 'application/octet-stream')
|
|
971
|
+
return resp.content
|
|
972
|
+
|
|
973
|
+
@app.post('/captcha/decrypt-filecrypt-circle')
|
|
974
|
+
def proxy_form_submit():
|
|
975
|
+
url = request.query.get('url')
|
|
976
|
+
session_id = request.query.get('session_id')
|
|
977
|
+
package_id = request.query.get('package_id')
|
|
978
|
+
success = False
|
|
979
|
+
|
|
980
|
+
if not url or not session_id or not package_id:
|
|
981
|
+
response.status = 400
|
|
982
|
+
return "Missing required parameters"
|
|
983
|
+
|
|
984
|
+
cookies = {'PHPSESSID': session_id}
|
|
985
|
+
|
|
986
|
+
headers = {
|
|
987
|
+
'User-Agent': shared_state.values["user_agent"],
|
|
988
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
raw_body = request.body.read()
|
|
992
|
+
|
|
993
|
+
resp = requests.post(url, cookies=cookies, headers=headers, data=raw_body, verify=False)
|
|
994
|
+
response.content_type = resp.headers.get('Content-Type', 'text/html')
|
|
995
|
+
|
|
996
|
+
if "<h2>Security Check</h2>" in resp.text or "click inside the open circle" in resp.text:
|
|
997
|
+
status = "CAPTCHA verification failed. Please try again."
|
|
998
|
+
info(status)
|
|
999
|
+
|
|
1000
|
+
match = re.search(
|
|
1001
|
+
r"top\.location\.href\s*=\s*['\"]([^'\"]*?/go\b[^'\"]*)['\"]",
|
|
1002
|
+
resp.text,
|
|
1003
|
+
re.IGNORECASE
|
|
1004
|
+
)
|
|
1005
|
+
if match:
|
|
1006
|
+
redirect = match.group(1)
|
|
1007
|
+
resolved_url = urljoin(url, redirect)
|
|
1008
|
+
info(f"Redirect URL: {resolved_url}")
|
|
1009
|
+
try:
|
|
1010
|
+
redirect_resp = requests.post(resolved_url, cookies=cookies, headers=headers, allow_redirects=True,
|
|
1011
|
+
timeout=10, verify=False)
|
|
1012
|
+
|
|
1013
|
+
if "expired" in redirect_resp.text.lower():
|
|
1014
|
+
status = f"The CAPTCHA session has expired. Deleting package: {package_id}"
|
|
1015
|
+
info(status)
|
|
1016
|
+
shared_state.get_db("protected").delete(package_id)
|
|
1017
|
+
else:
|
|
1018
|
+
download_link = redirect_resp.url
|
|
1019
|
+
if redirect_resp.ok:
|
|
1020
|
+
status = f"Successfully resolved download link!"
|
|
1021
|
+
info(status)
|
|
1022
|
+
|
|
1023
|
+
raw_data = shared_state.get_db("protected").retrieve(package_id)
|
|
1024
|
+
data = json.loads(raw_data)
|
|
1025
|
+
title = data.get("title")
|
|
1026
|
+
password = data.get("password", "")
|
|
1027
|
+
links = [download_link]
|
|
1028
|
+
downloaded = shared_state.download_package(links, title, password, package_id)
|
|
1029
|
+
if downloaded:
|
|
1030
|
+
StatsHelper(shared_state).increment_package_with_links(links)
|
|
1031
|
+
success = True
|
|
1032
|
+
shared_state.get_db("protected").delete(package_id)
|
|
1033
|
+
else:
|
|
1034
|
+
raise RuntimeError("Submitting Download to JDownloader failed")
|
|
1035
|
+
else:
|
|
1036
|
+
info(
|
|
1037
|
+
f"Failed to reach redirect target. Status: {redirect_resp.status_code}, Solution: {status}")
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
info(f"Error while resolving download link: {e}")
|
|
1040
|
+
else:
|
|
1041
|
+
if resp.url.endswith("404.html"):
|
|
1042
|
+
info("Your IP has been blocked by Filecrypt. Please try again later.")
|
|
1043
|
+
else:
|
|
1044
|
+
info("You did not solve the CAPTCHA correctly. Please try again.")
|
|
1045
|
+
|
|
1046
|
+
if success:
|
|
1047
|
+
StatsHelper(shared_state).increment_captcha_decryptions_manual()
|
|
1048
|
+
else:
|
|
1049
|
+
StatsHelper(shared_state).increment_failed_decryptions_manual()
|
|
1050
|
+
|
|
1051
|
+
# Check if there are more CAPTCHAs to solve
|
|
1052
|
+
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
1053
|
+
has_more_captchas = bool(remaining_protected)
|
|
1054
|
+
|
|
1055
|
+
if has_more_captchas:
|
|
1056
|
+
solve_button = render_button("Solve another CAPTCHA", "primary", {
|
|
1057
|
+
"onclick": "location.href='/captcha'",
|
|
1058
|
+
})
|
|
1059
|
+
else:
|
|
1060
|
+
solve_button = "<b>No more CAPTCHAs</b>"
|
|
1061
|
+
|
|
1062
|
+
return render_centered_html(f"""
|
|
1063
|
+
<!DOCTYPE html>
|
|
1064
|
+
<html>
|
|
1065
|
+
<body>
|
|
1066
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
1067
|
+
<p>{status}</p>
|
|
1068
|
+
<p>
|
|
1069
|
+
{solve_button}
|
|
1070
|
+
</p>
|
|
1071
|
+
<p>
|
|
1072
|
+
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
1073
|
+
</p>
|
|
1074
|
+
</body>
|
|
1075
|
+
</html>""")
|