quasarr 0.1.6__py3-none-any.whl → 1.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of quasarr might be problematic. Click here for more details.

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