quasarr 1.3.5__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.

Files changed (67) hide show
  1. quasarr/__init__.py +157 -56
  2. quasarr/api/__init__.py +141 -36
  3. quasarr/api/arr/__init__.py +197 -78
  4. quasarr/api/captcha/__init__.py +897 -42
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +84 -22
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +237 -434
  9. quasarr/downloads/linkcrypters/al.py +237 -0
  10. quasarr/downloads/linkcrypters/filecrypt.py +178 -31
  11. quasarr/downloads/linkcrypters/hide.py +123 -0
  12. quasarr/downloads/packages/__init__.py +461 -0
  13. quasarr/downloads/sources/al.py +697 -0
  14. quasarr/downloads/sources/by.py +106 -0
  15. quasarr/downloads/sources/dd.py +6 -78
  16. quasarr/downloads/sources/dj.py +7 -0
  17. quasarr/downloads/sources/dt.py +1 -1
  18. quasarr/downloads/sources/dw.py +2 -2
  19. quasarr/downloads/sources/he.py +112 -0
  20. quasarr/downloads/sources/mb.py +47 -0
  21. quasarr/downloads/sources/nk.py +51 -0
  22. quasarr/downloads/sources/nx.py +36 -81
  23. quasarr/downloads/sources/sf.py +27 -4
  24. quasarr/downloads/sources/sj.py +7 -0
  25. quasarr/downloads/sources/sl.py +90 -0
  26. quasarr/downloads/sources/wd.py +110 -0
  27. quasarr/providers/cloudflare.py +204 -0
  28. quasarr/providers/html_images.py +20 -0
  29. quasarr/providers/html_templates.py +210 -108
  30. quasarr/providers/imdb_metadata.py +15 -2
  31. quasarr/providers/myjd_api.py +36 -5
  32. quasarr/providers/notifications.py +30 -5
  33. quasarr/providers/obfuscated.py +35 -0
  34. quasarr/providers/sessions/__init__.py +0 -0
  35. quasarr/providers/sessions/al.py +286 -0
  36. quasarr/providers/sessions/dd.py +78 -0
  37. quasarr/providers/sessions/nx.py +76 -0
  38. quasarr/providers/shared_state.py +368 -23
  39. quasarr/providers/statistics.py +154 -0
  40. quasarr/providers/version.py +60 -1
  41. quasarr/search/__init__.py +112 -36
  42. quasarr/search/sources/al.py +448 -0
  43. quasarr/search/sources/by.py +203 -0
  44. quasarr/search/sources/dd.py +17 -6
  45. quasarr/search/sources/dj.py +213 -0
  46. quasarr/search/sources/dt.py +37 -7
  47. quasarr/search/sources/dw.py +27 -47
  48. quasarr/search/sources/fx.py +27 -29
  49. quasarr/search/sources/he.py +196 -0
  50. quasarr/search/sources/mb.py +195 -0
  51. quasarr/search/sources/nk.py +188 -0
  52. quasarr/search/sources/nx.py +22 -6
  53. quasarr/search/sources/sf.py +143 -151
  54. quasarr/search/sources/sj.py +213 -0
  55. quasarr/search/sources/sl.py +246 -0
  56. quasarr/search/sources/wd.py +208 -0
  57. quasarr/storage/config.py +20 -4
  58. quasarr/storage/setup.py +224 -56
  59. quasarr-1.20.4.dist-info/METADATA +304 -0
  60. quasarr-1.20.4.dist-info/RECORD +72 -0
  61. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
  62. quasarr/providers/tvmaze_metadata.py +0 -23
  63. quasarr-1.3.5.dist-info/METADATA +0 -174
  64. quasarr-1.3.5.dist-info/RECORD +0 -43
  65. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
  66. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
  67. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
@@ -4,31 +4,43 @@
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
- from quasarr.downloads.linkcrypters.filecrypt import get_filecrypt_links
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 serve_captcha():
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>Quasarr</h1>
27
- <p>JDownloader connection not established.</p>''')
35
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
36
+ <p>JDownloader connection not established.</p>
37
+ <p>
38
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
39
+ </p>''')
28
40
 
29
41
  protected = shared_state.get_db("protected").retrieve_all_titles()
30
42
  if not protected:
31
- return render_centered_html(f'''<h1>Quasarr</h1>
43
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
32
44
  <p>No protected packages found! CAPTCHA not needed.</p>
33
45
  <p>
34
46
  {render_button("Confirm", "secondary", {"onclick": "location.href='/'"})}
@@ -45,9 +57,487 @@ def setup_captcha_routes(app):
45
57
  except KeyError:
46
58
  desired_mirror = None
47
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
+
48
538
  link_options = ""
49
- if len(links) > 1:
50
- for link in links:
539
+ if len(prioritized_links) > 1:
540
+ for link in prioritized_links:
51
541
  if "filecrypt." in link[0]:
52
542
  link_options += f'<option value="{link[0]}">{link[1]}</option>'
53
543
  link_select = f'''<div id="mirrors-select">
@@ -64,20 +554,38 @@ def setup_captcha_routes(app):
64
554
  </script>
65
555
  '''
66
556
  else:
67
- link_select = f'<div id="mirrors-select">Mirror: <b>{links[0][1]}</b></div>'
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)
68
567
 
69
568
  content = render_centered_html(r'''
70
569
  <script type="text/javascript">
71
570
  var api_key = "''' + captcha_values()["api_key"] + r'''";
72
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
+
73
575
  function handleToken(token) {
74
576
  document.getElementById("puzzle-captcha").remove();
75
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
+
76
586
  document.getElementById("captcha-key").innerText = 'Using result "' + token + '" to decrypt links...';
77
587
  var link = document.getElementById("link-hidden").value;
78
- const currentPath = window.location.pathname;
79
- const endpoint = '/decrypt-filecrypt';
80
- const fullPath = currentPath.endsWith('/') ? currentPath + endpoint.slice(1) : currentPath + endpoint;
588
+ const fullPath = '/captcha/decrypt-filecrypt';
81
589
 
82
590
  fetch(fullPath, {
83
591
  method: 'POST',
@@ -87,7 +595,7 @@ def setup_captcha_routes(app):
87
595
  body: JSON.stringify({
88
596
  token: token,
89
597
  ''' + f'''package_id: '{package_id}',
90
- title: '{title}',
598
+ title: '{js_single_quoted_string_safe(title)}',
91
599
  link: link,
92
600
  password: '{password}',
93
601
  mirror: '{desired_mirror}',
@@ -97,29 +605,50 @@ def setup_captcha_routes(app):
97
605
  .then(data => {
98
606
  if (data.success) {
99
607
  document.getElementById("captcha-key").insertAdjacentHTML('afterend',
100
- '<p>Successful for: ' + data.title + '</p>');
608
+ '<p>✅ Successful!</p>');
101
609
  } else {
102
610
  document.getElementById("captcha-key").insertAdjacentHTML('afterend',
103
611
  '<p>Failed. Check console for details!</p>');
104
612
  }
105
- document.getElementById("reload-button").style.display = "block";
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";
106
622
  });
107
623
  }
108
624
  ''' + captcha_js() + f'''</script>
109
625
  <div>
110
- <h1>Quasarr</h1>
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>
111
628
  <div id="captcha-key"></div>
112
629
  {link_select}<br><br>
113
- <input type="hidden" id="link-hidden" value="{links[0][0]}" />
114
- <div id="puzzle-captcha" aria-style="mobile">
115
- <strong>Your adblocker prevents the captcha from loading. Disable it!</strong>
630
+ <input type="hidden" id="link-hidden" value="{prioritized_links[0][0]}" />
631
+ <div class="captcha-container">
632
+ <div id="puzzle-captcha" aria-style="mobile">
633
+ <strong>Your adblocker prevents the captcha from loading. Disable it!</strong>
634
+ </div>
116
635
  </div>
117
636
  <div id="reload-button" style="display: none;">
118
- <p>
119
- {render_button("Solve another CAPTCHA", "secondary", {
120
- "onclick": "location.reload()",
121
- })}</p>
122
- </div>
637
+ </div>
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">
645
+ <p>
646
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
647
+ </p>
648
+ </div>
649
+ <div id="bypass-section">
650
+ {bypass_section}
651
+ </div>
123
652
  </div>
124
653
  </html>''')
125
654
 
@@ -131,13 +660,17 @@ def setup_captcha_routes(app):
131
660
 
132
661
  headers = {key: value for key, value in request.headers.items() if key != 'Host'}
133
662
  data = request.body.read()
134
- resp = requests.post(target_url, headers=headers, data=data)
663
+ resp = requests.post(target_url, headers=headers, data=data, verify=False)
135
664
 
136
665
  response.content_type = resp.headers.get('Content-Type')
137
666
 
138
667
  content = resp.text
139
- content = re.sub(r'<script src="/(.*?)"></script>',
140
- f'<script src="{captcha_values()["url"]}/\\1"></script>', content)
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
+
141
674
  response.content_type = 'text/html'
142
675
  return content
143
676
 
@@ -147,17 +680,30 @@ def setup_captcha_routes(app):
147
680
 
148
681
  headers = {key: value for key, value in request.headers.items() if key != 'Host'}
149
682
  data = request.body.read()
150
- resp = requests.post(target_url, headers=headers, data=data)
683
+ resp = requests.post(target_url, headers=headers, data=data, verify=False)
151
684
 
152
685
  response.content_type = resp.headers.get('Content-Type')
153
686
  return resp.content
154
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
+
155
701
  @app.get('/captcha/<captcha_id>/<uuid>/<filename>')
156
702
  def proxy_pngs(captcha_id, uuid, filename):
157
703
  new_url = f"{captcha_values()["url"]}/captcha/{captcha_id}/{uuid}/{filename}"
158
704
 
159
705
  try:
160
- external_response = requests.get(new_url, stream=True)
706
+ external_response = requests.get(new_url, stream=True, verify=False)
161
707
  external_response.raise_for_status()
162
708
  response.content_type = 'image/png'
163
709
  response.headers['Content-Disposition'] = f'inline; filename="{filename}"'
@@ -173,7 +719,7 @@ def setup_captcha_routes(app):
173
719
  headers = {key: value for key, value in request.headers.items()}
174
720
 
175
721
  data = request.body.read()
176
- resp = requests.post(new_url, headers=headers, data=data)
722
+ resp = requests.post(new_url, headers=headers, data=data, verify=False)
177
723
 
178
724
  response.status = resp.status_code
179
725
  for header in resp.headers:
@@ -181,14 +727,123 @@ def setup_captcha_routes(app):
181
727
  response.set_header(header, resp.headers[header])
182
728
  return resp.content
183
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
+
184
839
  @app.post('/captcha/decrypt-filecrypt')
185
840
  def submit_token():
186
841
  protected = shared_state.get_db("protected").retrieve_all_titles()
187
842
  if not protected:
188
843
  return {"success": False, "title": "No protected packages found! CAPTCHA not needed."}
189
844
 
190
- download_links = []
191
-
845
+ links = []
846
+ title = "Unknown Package"
192
847
  try:
193
848
  data = request.json
194
849
  token = data.get('token')
@@ -201,20 +856,220 @@ def setup_captcha_routes(app):
201
856
  if token:
202
857
  info(f"Received token: {token}")
203
858
  info(f"Decrypting links for {title}")
204
- download_links = get_filecrypt_links(shared_state, token, title, link, password=password, mirror=mirror)
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")
205
865
 
206
- info(f"Decrypted {len(download_links)} download links for {title}")
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}")
207
880
 
208
- if download_links:
209
- downloaded = shared_state.download_package(download_links, title, password, package_id)
210
- if downloaded:
211
- shared_state.get_db("protected").delete(package_id)
212
881
  else:
213
- raise RuntimeError("Submitting Download to JDownloader failed")
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")
214
893
  else:
215
894
  raise ValueError("No download links found")
216
895
 
217
896
  except Exception as e:
218
897
  info(f"Error decrypting: {e}")
219
898
 
220
- return {"success": bool(download_links), "title": title}
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>""")