quasarr 2.4.8__py3-none-any.whl → 2.4.9__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.
- quasarr/__init__.py +134 -70
- quasarr/api/__init__.py +40 -31
- quasarr/api/arr/__init__.py +116 -108
- quasarr/api/captcha/__init__.py +262 -137
- quasarr/api/config/__init__.py +76 -46
- quasarr/api/packages/__init__.py +138 -102
- quasarr/api/sponsors_helper/__init__.py +29 -16
- quasarr/api/statistics/__init__.py +19 -19
- quasarr/downloads/__init__.py +165 -72
- quasarr/downloads/linkcrypters/al.py +35 -18
- quasarr/downloads/linkcrypters/filecrypt.py +107 -52
- quasarr/downloads/linkcrypters/hide.py +5 -6
- quasarr/downloads/packages/__init__.py +342 -177
- quasarr/downloads/sources/al.py +191 -100
- quasarr/downloads/sources/by.py +31 -13
- quasarr/downloads/sources/dd.py +27 -14
- quasarr/downloads/sources/dj.py +1 -3
- quasarr/downloads/sources/dl.py +126 -71
- quasarr/downloads/sources/dt.py +11 -5
- quasarr/downloads/sources/dw.py +28 -14
- quasarr/downloads/sources/he.py +32 -24
- quasarr/downloads/sources/mb.py +19 -9
- quasarr/downloads/sources/nk.py +14 -10
- quasarr/downloads/sources/nx.py +8 -18
- quasarr/downloads/sources/sf.py +45 -20
- quasarr/downloads/sources/sj.py +1 -3
- quasarr/downloads/sources/sl.py +9 -5
- quasarr/downloads/sources/wd.py +32 -12
- quasarr/downloads/sources/wx.py +35 -21
- quasarr/providers/auth.py +42 -37
- quasarr/providers/cloudflare.py +28 -30
- quasarr/providers/hostname_issues.py +2 -1
- quasarr/providers/html_images.py +2 -2
- quasarr/providers/html_templates.py +22 -14
- quasarr/providers/imdb_metadata.py +149 -80
- quasarr/providers/jd_cache.py +131 -39
- quasarr/providers/log.py +1 -1
- quasarr/providers/myjd_api.py +260 -196
- quasarr/providers/notifications.py +53 -41
- quasarr/providers/obfuscated.py +9 -4
- quasarr/providers/sessions/al.py +71 -55
- quasarr/providers/sessions/dd.py +21 -14
- quasarr/providers/sessions/dl.py +30 -19
- quasarr/providers/sessions/nx.py +23 -14
- quasarr/providers/shared_state.py +292 -141
- quasarr/providers/statistics.py +75 -43
- quasarr/providers/utils.py +33 -27
- quasarr/providers/version.py +45 -14
- quasarr/providers/web_server.py +10 -5
- quasarr/search/__init__.py +30 -18
- quasarr/search/sources/al.py +124 -73
- quasarr/search/sources/by.py +110 -59
- quasarr/search/sources/dd.py +57 -35
- quasarr/search/sources/dj.py +69 -48
- quasarr/search/sources/dl.py +159 -100
- quasarr/search/sources/dt.py +110 -74
- quasarr/search/sources/dw.py +121 -61
- quasarr/search/sources/fx.py +108 -62
- quasarr/search/sources/he.py +78 -49
- quasarr/search/sources/mb.py +96 -48
- quasarr/search/sources/nk.py +80 -50
- quasarr/search/sources/nx.py +91 -62
- quasarr/search/sources/sf.py +171 -106
- quasarr/search/sources/sj.py +69 -48
- quasarr/search/sources/sl.py +115 -71
- quasarr/search/sources/wd.py +67 -44
- quasarr/search/sources/wx.py +188 -123
- quasarr/storage/config.py +65 -52
- quasarr/storage/setup.py +238 -140
- quasarr/storage/sqlite_database.py +10 -4
- {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
- quasarr-2.4.9.dist-info/RECORD +81 -0
- quasarr-2.4.8.dist-info/RECORD +0 -81
- {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
- {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.8.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/api/captcha/__init__.py
CHANGED
|
@@ -4,24 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
-
from base64 import
|
|
7
|
+
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
|
8
8
|
from urllib.parse import quote, unquote
|
|
9
9
|
|
|
10
10
|
import requests
|
|
11
|
-
from bottle import
|
|
11
|
+
from bottle import HTTPResponse, redirect, request, response
|
|
12
12
|
|
|
13
13
|
import quasarr.providers.html_images as images
|
|
14
|
-
from quasarr.downloads.linkcrypters.filecrypt import
|
|
14
|
+
from quasarr.downloads.linkcrypters.filecrypt import DLC, get_filecrypt_links
|
|
15
15
|
from quasarr.downloads.packages import delete_package
|
|
16
|
-
from quasarr.providers import obfuscated
|
|
17
|
-
from quasarr.providers import shared_state
|
|
16
|
+
from quasarr.providers import obfuscated, shared_state
|
|
18
17
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
19
|
-
from quasarr.providers.log import
|
|
18
|
+
from quasarr.providers.log import debug, info
|
|
20
19
|
from quasarr.providers.statistics import StatsHelper
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
def js_single_quoted_string_safe(text):
|
|
24
|
-
return text.replace(
|
|
23
|
+
return text.replace("\\", "\\\\").replace("'", "\\'")
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def check_package_exists(package_id):
|
|
@@ -35,12 +34,12 @@ def check_package_exists(package_id):
|
|
|
35
34
|
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
36
35
|
</p>
|
|
37
36
|
'''),
|
|
38
|
-
content_type="text/html"
|
|
37
|
+
content_type="text/html",
|
|
39
38
|
)
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def setup_captcha_routes(app):
|
|
43
|
-
@app.get(
|
|
42
|
+
@app.get("/captcha")
|
|
44
43
|
def check_captcha():
|
|
45
44
|
try:
|
|
46
45
|
device = shared_state.values["device"]
|
|
@@ -66,7 +65,7 @@ def setup_captcha_routes(app):
|
|
|
66
65
|
</p>''')
|
|
67
66
|
else:
|
|
68
67
|
# Check if a specific package_id was requested
|
|
69
|
-
requested_package_id = request.query.get(
|
|
68
|
+
requested_package_id = request.query.get("package_id")
|
|
70
69
|
package = None
|
|
71
70
|
|
|
72
71
|
if requested_package_id:
|
|
@@ -114,7 +113,9 @@ def setup_captcha_routes(app):
|
|
|
114
113
|
def is_junkies_link(link):
|
|
115
114
|
"""Check if link is a junkies link (handles [[url, mirror]] format)."""
|
|
116
115
|
url = link[0] if isinstance(link, (list, tuple)) else link
|
|
117
|
-
mirror =
|
|
116
|
+
mirror = (
|
|
117
|
+
link[1] if isinstance(link, (list, tuple)) and len(link) > 1 else ""
|
|
118
|
+
)
|
|
118
119
|
if mirror == "junkies":
|
|
119
120
|
return True
|
|
120
121
|
return (sj and sj in url) or (dj and dj in url)
|
|
@@ -123,19 +124,31 @@ def setup_captcha_routes(app):
|
|
|
123
124
|
|
|
124
125
|
# Hide uses nested arrays like FileCrypt: [["url", "mirror"]]
|
|
125
126
|
has_hide_links = any(
|
|
126
|
-
(
|
|
127
|
+
(
|
|
128
|
+
"hide." in link[0]
|
|
129
|
+
if isinstance(link, (list, tuple))
|
|
130
|
+
else "hide." in link
|
|
131
|
+
)
|
|
127
132
|
for link in prioritized_links
|
|
128
133
|
)
|
|
129
134
|
|
|
130
135
|
# KeepLinks uses nested arrays like FileCrypt: [["url", "mirror"]]
|
|
131
136
|
has_keeplinks_links = any(
|
|
132
|
-
(
|
|
137
|
+
(
|
|
138
|
+
"keeplinks." in link[0]
|
|
139
|
+
if isinstance(link, (list, tuple))
|
|
140
|
+
else "keeplinks." in link
|
|
141
|
+
)
|
|
133
142
|
for link in prioritized_links
|
|
134
143
|
)
|
|
135
144
|
|
|
136
145
|
# ToLink uses nested arrays like FileCrypt: [["url", "mirror"]]
|
|
137
146
|
has_tolink_links = any(
|
|
138
|
-
(
|
|
147
|
+
(
|
|
148
|
+
"tolink." in link[0]
|
|
149
|
+
if isinstance(link, (list, tuple))
|
|
150
|
+
else "tolink." in link
|
|
151
|
+
)
|
|
139
152
|
for link in prioritized_links
|
|
140
153
|
)
|
|
141
154
|
|
|
@@ -162,14 +175,16 @@ def setup_captcha_routes(app):
|
|
|
162
175
|
</p>''')
|
|
163
176
|
|
|
164
177
|
def decode_payload():
|
|
165
|
-
encoded = request.query.get(
|
|
178
|
+
encoded = request.query.get("data")
|
|
166
179
|
try:
|
|
167
180
|
decoded = urlsafe_b64decode(unquote(encoded)).decode()
|
|
168
181
|
return json.loads(decoded)
|
|
169
182
|
except Exception as e:
|
|
170
183
|
return {"error": f"Failed to decode payload: {str(e)}"}
|
|
171
184
|
|
|
172
|
-
def render_userscript_section(
|
|
185
|
+
def render_userscript_section(
|
|
186
|
+
url, package_id, title, password, provider_type="junkies"
|
|
187
|
+
):
|
|
173
188
|
"""Render the userscript UI section for Junkies, KeepLinks, ToLink, or Hide pages
|
|
174
189
|
|
|
175
190
|
This is the MAIN solution for these providers (not a bypass/fallback).
|
|
@@ -182,13 +197,18 @@ def setup_captcha_routes(app):
|
|
|
182
197
|
provider_type: Either "hide", "junkies", "keeplinks", or "tolink"
|
|
183
198
|
"""
|
|
184
199
|
|
|
185
|
-
provider_names = {
|
|
200
|
+
provider_names = {
|
|
201
|
+
"hide": "Hide",
|
|
202
|
+
"junkies": "Junkies",
|
|
203
|
+
"keeplinks": "KeepLinks",
|
|
204
|
+
"tolink": "ToLink",
|
|
205
|
+
}
|
|
186
206
|
provider_name = provider_names.get(provider_type, "Provider")
|
|
187
207
|
userscript_url = f"/captcha/{provider_type}.user.js"
|
|
188
208
|
storage_key = f"hide{provider_name}SetupInstructions"
|
|
189
209
|
|
|
190
210
|
# Generate userscript URL with transfer params
|
|
191
|
-
base_url = request.urlparts.scheme +
|
|
211
|
+
base_url = request.urlparts.scheme + "://" + request.urlparts.netloc
|
|
192
212
|
transfer_url = f"{base_url}/captcha/quick-transfer"
|
|
193
213
|
|
|
194
214
|
url_with_quick_transfer_params = (
|
|
@@ -324,7 +344,7 @@ def setup_captcha_routes(app):
|
|
|
324
344
|
|
|
325
345
|
source_button = ""
|
|
326
346
|
if original_url:
|
|
327
|
-
source_button = f
|
|
347
|
+
source_button = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
|
|
328
348
|
|
|
329
349
|
return render_centered_html(f"""
|
|
330
350
|
<!DOCTYPE html>
|
|
@@ -370,7 +390,7 @@ def setup_captcha_routes(app):
|
|
|
370
390
|
|
|
371
391
|
source_button = ""
|
|
372
392
|
if original_url:
|
|
373
|
-
source_button = f
|
|
393
|
+
source_button = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
|
|
374
394
|
|
|
375
395
|
return render_centered_html(f"""
|
|
376
396
|
<!DOCTYPE html>
|
|
@@ -417,7 +437,7 @@ def setup_captcha_routes(app):
|
|
|
417
437
|
|
|
418
438
|
source_button = ""
|
|
419
439
|
if original_url:
|
|
420
|
-
source_button = f
|
|
440
|
+
source_button = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
|
|
421
441
|
|
|
422
442
|
return render_centered_html(f"""
|
|
423
443
|
<!DOCTYPE html>
|
|
@@ -464,7 +484,7 @@ def setup_captcha_routes(app):
|
|
|
464
484
|
|
|
465
485
|
source_button = ""
|
|
466
486
|
if original_url:
|
|
467
|
-
source_button = f
|
|
487
|
+
source_button = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
|
|
468
488
|
|
|
469
489
|
return render_centered_html(f"""
|
|
470
490
|
<!DOCTYPE html>
|
|
@@ -485,37 +505,37 @@ def setup_captcha_routes(app):
|
|
|
485
505
|
</body>
|
|
486
506
|
</html>""")
|
|
487
507
|
|
|
488
|
-
@app.get(
|
|
508
|
+
@app.get("/captcha/filecrypt.user.js")
|
|
489
509
|
def serve_filecrypt_user_js():
|
|
490
510
|
content = obfuscated.filecrypt_user_js()
|
|
491
|
-
response.content_type =
|
|
511
|
+
response.content_type = "application/javascript"
|
|
492
512
|
return content
|
|
493
513
|
|
|
494
|
-
@app.get(
|
|
514
|
+
@app.get("/captcha/hide.user.js")
|
|
495
515
|
def serve_hide_user_js():
|
|
496
516
|
content = obfuscated.hide_user_js()
|
|
497
|
-
response.content_type =
|
|
517
|
+
response.content_type = "application/javascript"
|
|
498
518
|
return content
|
|
499
519
|
|
|
500
|
-
@app.get(
|
|
520
|
+
@app.get("/captcha/junkies.user.js")
|
|
501
521
|
def serve_junkies_user_js():
|
|
502
522
|
sj = shared_state.values["config"]("Hostnames").get("sj")
|
|
503
523
|
dj = shared_state.values["config"]("Hostnames").get("dj")
|
|
504
524
|
|
|
505
525
|
content = obfuscated.junkies_user_js(sj, dj)
|
|
506
|
-
response.content_type =
|
|
526
|
+
response.content_type = "application/javascript"
|
|
507
527
|
return content
|
|
508
528
|
|
|
509
|
-
@app.get(
|
|
529
|
+
@app.get("/captcha/keeplinks.user.js")
|
|
510
530
|
def serve_keeplinks_user_js():
|
|
511
531
|
content = obfuscated.keeplinks_user_js()
|
|
512
|
-
response.content_type =
|
|
532
|
+
response.content_type = "application/javascript"
|
|
513
533
|
return content
|
|
514
534
|
|
|
515
|
-
@app.get(
|
|
535
|
+
@app.get("/captcha/tolink.user.js")
|
|
516
536
|
def serve_tolink_user_js():
|
|
517
537
|
content = obfuscated.tolink_user_js()
|
|
518
|
-
response.content_type =
|
|
538
|
+
response.content_type = "application/javascript"
|
|
519
539
|
return content
|
|
520
540
|
|
|
521
541
|
def render_filecrypt_bypass_section(url, package_id, title, password):
|
|
@@ -523,7 +543,7 @@ def setup_captcha_routes(app):
|
|
|
523
543
|
|
|
524
544
|
# Generate userscript URL with transfer params
|
|
525
545
|
# Get base URL of current request
|
|
526
|
-
base_url = request.urlparts.scheme +
|
|
546
|
+
base_url = request.urlparts.scheme + "://" + request.urlparts.netloc
|
|
527
547
|
transfer_url = f"{base_url}/captcha/quick-transfer"
|
|
528
548
|
|
|
529
549
|
url_with_quick_transfer_params = (
|
|
@@ -648,11 +668,11 @@ def setup_captcha_routes(app):
|
|
|
648
668
|
# Single package - just show the title without dropdown
|
|
649
669
|
if len(protected) <= 1:
|
|
650
670
|
if current_title:
|
|
651
|
-
return f
|
|
671
|
+
return f"""
|
|
652
672
|
<div class="package-selector" style="margin-bottom: 20px; padding: 12px; background: rgba(128, 128, 128, 0.1); border: 1px solid rgba(128, 128, 128, 0.3); border-radius: 8px;">
|
|
653
673
|
<p style="margin: 0; word-break: break-all;"><b>📦 Package:</b> {current_title}</p>
|
|
654
674
|
</div>
|
|
655
|
-
|
|
675
|
+
"""
|
|
656
676
|
return ""
|
|
657
677
|
|
|
658
678
|
sj = shared_state.values["config"]("Hostnames").get("sj")
|
|
@@ -660,17 +680,28 @@ def setup_captcha_routes(app):
|
|
|
660
680
|
|
|
661
681
|
def is_junkies_link(link):
|
|
662
682
|
url = link[0] if isinstance(link, (list, tuple)) else link
|
|
663
|
-
mirror =
|
|
683
|
+
mirror = (
|
|
684
|
+
link[1] if isinstance(link, (list, tuple)) and len(link) > 1 else ""
|
|
685
|
+
)
|
|
664
686
|
if mirror == "junkies":
|
|
665
687
|
return True
|
|
666
688
|
return (sj and sj in url) or (dj and dj in url)
|
|
667
689
|
|
|
668
690
|
def get_captcha_type_for_links(links):
|
|
669
691
|
"""Determine which captcha type to use based on links"""
|
|
670
|
-
has_hide = any(
|
|
692
|
+
has_hide = any(
|
|
693
|
+
("hide." in (l[0] if isinstance(l, (list, tuple)) else l))
|
|
694
|
+
for l in links
|
|
695
|
+
)
|
|
671
696
|
has_junkies = any(is_junkies_link(l) for l in links)
|
|
672
|
-
has_keeplinks = any(
|
|
673
|
-
|
|
697
|
+
has_keeplinks = any(
|
|
698
|
+
("keeplinks." in (l[0] if isinstance(l, (list, tuple)) else l))
|
|
699
|
+
for l in links
|
|
700
|
+
)
|
|
701
|
+
has_tolink = any(
|
|
702
|
+
("tolink." in (l[0] if isinstance(l, (list, tuple)) else l))
|
|
703
|
+
for l in links
|
|
704
|
+
)
|
|
674
705
|
|
|
675
706
|
if has_hide:
|
|
676
707
|
return "hide"
|
|
@@ -712,11 +743,13 @@ def setup_captcha_routes(app):
|
|
|
712
743
|
selected = "selected" if pkg_id == current_package_id else ""
|
|
713
744
|
# Truncate long titles for display
|
|
714
745
|
display_title = (title[:50] + "...") if len(title) > 53 else title
|
|
715
|
-
options.append(
|
|
746
|
+
options.append(
|
|
747
|
+
f'<option value="{captcha_type}|{quote(encoded)}" {selected}>{display_title}</option>'
|
|
748
|
+
)
|
|
716
749
|
|
|
717
750
|
options_html = "\n".join(options)
|
|
718
751
|
|
|
719
|
-
return f
|
|
752
|
+
return f"""
|
|
720
753
|
<div class="package-selector" style="margin-bottom: 20px; padding: 12px; background: rgba(128, 128, 128, 0.1); border: 1px solid rgba(128, 128, 128, 0.3); border-radius: 8px;">
|
|
721
754
|
<label for="package-select" style="display: block; margin-bottom: 8px; font-weight: bold;">📦 Select Package:</label>
|
|
722
755
|
<select id="package-select" style="width: 100%; padding: 8px; border-radius: 4px; background: inherit; color: inherit; border: 1px solid rgba(128, 128, 128, 0.5); cursor: pointer;">
|
|
@@ -729,9 +762,11 @@ def setup_captcha_routes(app):
|
|
|
729
762
|
window.location.href = '/captcha/' + captchaType + '?data=' + encodedData;
|
|
730
763
|
}});
|
|
731
764
|
</script>
|
|
732
|
-
|
|
765
|
+
"""
|
|
733
766
|
|
|
734
|
-
def render_failed_attempts_warning(
|
|
767
|
+
def render_failed_attempts_warning(
|
|
768
|
+
package_id, include_delete_button=True, fallback_url=None
|
|
769
|
+
):
|
|
735
770
|
"""Render a warning block that shows after 2+ failed attempts per package_id.
|
|
736
771
|
Uses localStorage to track attempts by package_id to ensure reliable tracking
|
|
737
772
|
even when package titles are duplicated.
|
|
@@ -748,8 +783,11 @@ def setup_captcha_routes(app):
|
|
|
748
783
|
|
|
749
784
|
delete_button = ""
|
|
750
785
|
if include_delete_button:
|
|
751
|
-
delete_button = render_button(
|
|
752
|
-
|
|
786
|
+
delete_button = render_button(
|
|
787
|
+
"Delete Package",
|
|
788
|
+
"primary",
|
|
789
|
+
{"onclick": f"location.href='/captcha/delete/{package_id}'"},
|
|
790
|
+
)
|
|
753
791
|
|
|
754
792
|
fallback_link = ""
|
|
755
793
|
if fallback_url:
|
|
@@ -759,7 +797,7 @@ def setup_captcha_routes(app):
|
|
|
759
797
|
</p>
|
|
760
798
|
'''
|
|
761
799
|
|
|
762
|
-
return f
|
|
800
|
+
return f"""
|
|
763
801
|
<div id="failed-attempts-warning" class="warning-box" style="display: none; background: #fee2e2; border: 2px solid #dc2626; border-radius: 8px; padding: 16px; margin-bottom: 20px; text-align: center; color: #991b1b;">
|
|
764
802
|
<h3 style="color: #dc2626; margin-top: 0;">⚠️ Multiple Failed Attempts Detected</h3>
|
|
765
803
|
<p style="margin-bottom: 12px; color: #7f1d1d;">This CAPTCHA has failed multiple times. The link may be <b>offline</b> or require a different solution method.</p>
|
|
@@ -811,7 +849,7 @@ def setup_captcha_routes(app):
|
|
|
811
849
|
}};
|
|
812
850
|
}})();
|
|
813
851
|
</script>
|
|
814
|
-
|
|
852
|
+
"""
|
|
815
853
|
|
|
816
854
|
@app.get("/captcha/filecrypt")
|
|
817
855
|
def serve_filecrypt_fallback():
|
|
@@ -836,7 +874,7 @@ def setup_captcha_routes(app):
|
|
|
836
874
|
url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
|
|
837
875
|
|
|
838
876
|
# Generate userscript URL with transfer params
|
|
839
|
-
base_url = request.urlparts.scheme +
|
|
877
|
+
base_url = request.urlparts.scheme + "://" + request.urlparts.netloc
|
|
840
878
|
transfer_url = f"{base_url}/captcha/quick-transfer"
|
|
841
879
|
|
|
842
880
|
url_with_quick_transfer_params = (
|
|
@@ -852,7 +890,7 @@ def setup_captcha_routes(app):
|
|
|
852
890
|
|
|
853
891
|
source_button = ""
|
|
854
892
|
if original_url:
|
|
855
|
-
source_button = f
|
|
893
|
+
source_button = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
|
|
856
894
|
|
|
857
895
|
return render_centered_html(f"""
|
|
858
896
|
<!DOCTYPE html>
|
|
@@ -977,14 +1015,14 @@ def setup_captcha_routes(app):
|
|
|
977
1015
|
</body>
|
|
978
1016
|
</html>""")
|
|
979
1017
|
|
|
980
|
-
@app.get(
|
|
1018
|
+
@app.get("/captcha/quick-transfer")
|
|
981
1019
|
def handle_quick_transfer():
|
|
982
1020
|
"""Handle quick transfer from userscript"""
|
|
983
1021
|
import zlib
|
|
984
1022
|
|
|
985
1023
|
try:
|
|
986
|
-
package_id = request.query.get(
|
|
987
|
-
compressed_links = request.query.get(
|
|
1024
|
+
package_id = request.query.get("pkg_id")
|
|
1025
|
+
compressed_links = request.query.get("links", "")
|
|
988
1026
|
|
|
989
1027
|
if not package_id or not compressed_links:
|
|
990
1028
|
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
@@ -997,7 +1035,7 @@ def setup_captcha_routes(app):
|
|
|
997
1035
|
# Add padding if needed
|
|
998
1036
|
padding = 4 - (len(compressed_links) % 4)
|
|
999
1037
|
if padding != 4:
|
|
1000
|
-
compressed_links +=
|
|
1038
|
+
compressed_links += "=" * padding
|
|
1001
1039
|
|
|
1002
1040
|
try:
|
|
1003
1041
|
decoded = urlsafe_b64decode(compressed_links)
|
|
@@ -1011,7 +1049,9 @@ def setup_captcha_routes(app):
|
|
|
1011
1049
|
|
|
1012
1050
|
# Decompress using zlib - use raw deflate format (no header)
|
|
1013
1051
|
try:
|
|
1014
|
-
decompressed = zlib.decompress(
|
|
1052
|
+
decompressed = zlib.decompress(
|
|
1053
|
+
decoded, -15
|
|
1054
|
+
) # -15 = raw deflate, no zlib header
|
|
1015
1055
|
except Exception as e:
|
|
1016
1056
|
debug(f"Decompression error: {e}, trying with header...")
|
|
1017
1057
|
try:
|
|
@@ -1025,14 +1065,16 @@ def setup_captcha_routes(app):
|
|
|
1025
1065
|
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
1026
1066
|
</p>''')
|
|
1027
1067
|
|
|
1028
|
-
links_text = decompressed.decode(
|
|
1068
|
+
links_text = decompressed.decode("utf-8")
|
|
1029
1069
|
|
|
1030
1070
|
# Parse links and restore protocols
|
|
1031
|
-
raw_links = [
|
|
1071
|
+
raw_links = [
|
|
1072
|
+
link.strip() for link in links_text.split("\n") if link.strip()
|
|
1073
|
+
]
|
|
1032
1074
|
links = []
|
|
1033
1075
|
for link in raw_links:
|
|
1034
|
-
if not link.startswith((
|
|
1035
|
-
link =
|
|
1076
|
+
if not link.startswith(("http://", "https://")):
|
|
1077
|
+
link = "https://" + link
|
|
1036
1078
|
links.append(link)
|
|
1037
1079
|
|
|
1038
1080
|
info(f"Quick transfer received {len(links)} links for package {package_id}")
|
|
@@ -1051,7 +1093,9 @@ def setup_captcha_routes(app):
|
|
|
1051
1093
|
password = data.get("password", "")
|
|
1052
1094
|
|
|
1053
1095
|
# Download the package
|
|
1054
|
-
downloaded = shared_state.download_package(
|
|
1096
|
+
downloaded = shared_state.download_package(
|
|
1097
|
+
links, title, password, package_id
|
|
1098
|
+
)
|
|
1055
1099
|
|
|
1056
1100
|
if downloaded:
|
|
1057
1101
|
StatsHelper(shared_state).increment_package_with_links(links)
|
|
@@ -1061,12 +1105,17 @@ def setup_captcha_routes(app):
|
|
|
1061
1105
|
info(f"Quick transfer successful: {len(links)} links processed")
|
|
1062
1106
|
|
|
1063
1107
|
# Check if more CAPTCHAs remain
|
|
1064
|
-
remaining_protected = shared_state.get_db(
|
|
1108
|
+
remaining_protected = shared_state.get_db(
|
|
1109
|
+
"protected"
|
|
1110
|
+
).retrieve_all_titles()
|
|
1065
1111
|
has_more_captchas = bool(remaining_protected)
|
|
1066
1112
|
|
|
1067
1113
|
if has_more_captchas:
|
|
1068
|
-
solve_button = render_button(
|
|
1069
|
-
|
|
1114
|
+
solve_button = render_button(
|
|
1115
|
+
"Solve another CAPTCHA",
|
|
1116
|
+
"primary",
|
|
1117
|
+
{"onclick": "location.href='/captcha'"},
|
|
1118
|
+
)
|
|
1070
1119
|
else:
|
|
1071
1120
|
solve_button = "<b>No more CAPTCHAs</b>"
|
|
1072
1121
|
|
|
@@ -1096,7 +1145,7 @@ def setup_captcha_routes(app):
|
|
|
1096
1145
|
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
1097
1146
|
</p>''')
|
|
1098
1147
|
|
|
1099
|
-
@app.get(
|
|
1148
|
+
@app.get("/captcha/delete/<package_id>")
|
|
1100
1149
|
def delete_captcha_package(package_id):
|
|
1101
1150
|
success = delete_package(shared_state, package_id)
|
|
1102
1151
|
|
|
@@ -1105,9 +1154,13 @@ def setup_captcha_routes(app):
|
|
|
1105
1154
|
has_more_captchas = bool(remaining_protected)
|
|
1106
1155
|
|
|
1107
1156
|
if has_more_captchas:
|
|
1108
|
-
solve_button = render_button(
|
|
1109
|
-
"
|
|
1110
|
-
|
|
1157
|
+
solve_button = render_button(
|
|
1158
|
+
"Solve another CAPTCHA",
|
|
1159
|
+
"primary",
|
|
1160
|
+
{
|
|
1161
|
+
"onclick": "location.href='/captcha'",
|
|
1162
|
+
},
|
|
1163
|
+
)
|
|
1111
1164
|
else:
|
|
1112
1165
|
solve_button = "<b>No more CAPTCHAs</b>"
|
|
1113
1166
|
|
|
@@ -1132,7 +1185,7 @@ def setup_captcha_routes(app):
|
|
|
1132
1185
|
</p>''')
|
|
1133
1186
|
|
|
1134
1187
|
# The following routes are for cutcaptcha
|
|
1135
|
-
@app.get(
|
|
1188
|
+
@app.get("/captcha/cutcaptcha")
|
|
1136
1189
|
def serve_cutcaptcha():
|
|
1137
1190
|
payload = decode_payload()
|
|
1138
1191
|
|
|
@@ -1171,7 +1224,7 @@ def setup_captcha_routes(app):
|
|
|
1171
1224
|
for link in prioritized_links:
|
|
1172
1225
|
if "filecrypt." in link[0]:
|
|
1173
1226
|
link_options += f'<option value="{link[0]}">{link[1]}</option>'
|
|
1174
|
-
link_select = f
|
|
1227
|
+
link_select = f"""<div id="mirrors-select">
|
|
1175
1228
|
<label for="link-select">Mirror:</label>
|
|
1176
1229
|
<select id="link-select">
|
|
1177
1230
|
{link_options}
|
|
@@ -1183,18 +1236,24 @@ def setup_captcha_routes(app):
|
|
|
1183
1236
|
document.getElementById("link-hidden").value = selectedLink;
|
|
1184
1237
|
}});
|
|
1185
1238
|
</script>
|
|
1186
|
-
|
|
1239
|
+
"""
|
|
1187
1240
|
else:
|
|
1188
1241
|
link_select = f'<div id="mirrors-select">Mirror: <b>{prioritized_links[0][1]}</b></div>'
|
|
1189
1242
|
|
|
1190
1243
|
# Pre-render button HTML in Python
|
|
1191
|
-
solve_another_html = render_button(
|
|
1192
|
-
|
|
1244
|
+
solve_another_html = render_button(
|
|
1245
|
+
"Solve another CAPTCHA", "primary", {"onclick": "location.href='/captcha'"}
|
|
1246
|
+
)
|
|
1247
|
+
back_button_html = render_button(
|
|
1248
|
+
"Back", "secondary", {"onclick": "location.href='/'"}
|
|
1249
|
+
)
|
|
1193
1250
|
|
|
1194
1251
|
url = prioritized_links[0][0]
|
|
1195
1252
|
|
|
1196
1253
|
# Add bypass section
|
|
1197
|
-
bypass_section = render_filecrypt_bypass_section(
|
|
1254
|
+
bypass_section = render_filecrypt_bypass_section(
|
|
1255
|
+
url, package_id, title, password
|
|
1256
|
+
)
|
|
1198
1257
|
|
|
1199
1258
|
# Add package selector and failed attempts warning
|
|
1200
1259
|
package_selector = render_package_selector(package_id, title)
|
|
@@ -1208,20 +1267,29 @@ def setup_captcha_routes(app):
|
|
|
1208
1267
|
"links": prioritized_links,
|
|
1209
1268
|
"original_url": original_url,
|
|
1210
1269
|
}
|
|
1211
|
-
fallback_encoded = urlsafe_b64encode(
|
|
1270
|
+
fallback_encoded = urlsafe_b64encode(
|
|
1271
|
+
json.dumps(fallback_payload).encode()
|
|
1272
|
+
).decode()
|
|
1212
1273
|
filecrypt_fallback_url = f"/captcha/filecrypt?data={quote(fallback_encoded)}"
|
|
1213
1274
|
|
|
1214
|
-
failed_warning = render_failed_attempts_warning(
|
|
1215
|
-
|
|
1275
|
+
failed_warning = render_failed_attempts_warning(
|
|
1276
|
+
package_id, include_delete_button=False, fallback_url=filecrypt_fallback_url
|
|
1277
|
+
) # Delete button is already below
|
|
1216
1278
|
|
|
1217
1279
|
# Escape title for safe use in JavaScript string
|
|
1218
|
-
escaped_title_js =
|
|
1280
|
+
escaped_title_js = (
|
|
1281
|
+
title.replace("\\", "\\\\")
|
|
1282
|
+
.replace('"', '\\"')
|
|
1283
|
+
.replace("\n", "\\n")
|
|
1284
|
+
.replace("\r", "\\r")
|
|
1285
|
+
)
|
|
1219
1286
|
|
|
1220
1287
|
source_button_html = ""
|
|
1221
1288
|
if original_url:
|
|
1222
|
-
source_button_html = f
|
|
1289
|
+
source_button_html = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
|
|
1223
1290
|
|
|
1224
|
-
content = render_centered_html(
|
|
1291
|
+
content = render_centered_html(
|
|
1292
|
+
r'''
|
|
1225
1293
|
<style>
|
|
1226
1294
|
/* Fix captcha container to shrink-wrap iframe on desktop */
|
|
1227
1295
|
.captcha-container {
|
|
@@ -1237,23 +1305,37 @@ def setup_captcha_routes(app):
|
|
|
1237
1305
|
</style>
|
|
1238
1306
|
<script type="text/javascript">
|
|
1239
1307
|
// Package title for result display
|
|
1240
|
-
var packageTitleText = "'''
|
|
1308
|
+
var packageTitleText = "'''
|
|
1309
|
+
+ escaped_title_js
|
|
1310
|
+
+ r"""";
|
|
1241
1311
|
|
|
1242
1312
|
// Check if we should redirect to fallback due to failed attempts
|
|
1243
1313
|
(function() {
|
|
1244
|
-
const storageKey = 'captcha_attempts_
|
|
1314
|
+
const storageKey = 'captcha_attempts_"""
|
|
1315
|
+
+ package_id
|
|
1316
|
+
+ r"""';
|
|
1245
1317
|
const attempts = parseInt(localStorage.getItem(storageKey) || '0', 10);
|
|
1246
1318
|
if (attempts >= 2) {
|
|
1247
1319
|
// Redirect to FileCrypt fallback page
|
|
1248
|
-
window.location.href =
|
|
1320
|
+
window.location.href = """
|
|
1321
|
+
" + filecrypt_fallback_url + r"
|
|
1322
|
+
''';
|
|
1249
1323
|
return;
|
|
1250
1324
|
}
|
|
1251
1325
|
})();
|
|
1252
1326
|
|
|
1253
|
-
var api_key = "'''
|
|
1327
|
+
var api_key = "'''
|
|
1328
|
+
+ obfuscated.captcha_values()["api_key"]
|
|
1329
|
+
+ r"""";
|
|
1254
1330
|
var endpoint = '/' + window.location.pathname.split('/')[1] + '/' + api_key + '.html';
|
|
1255
|
-
var solveAnotherHtml = `<p>
|
|
1256
|
-
|
|
1331
|
+
var solveAnotherHtml = `<p>"""
|
|
1332
|
+
+ solve_another_html
|
|
1333
|
+
+ r"""</p><p>"""
|
|
1334
|
+
+ back_button_html
|
|
1335
|
+
+ r"""</p>`;
|
|
1336
|
+
var noMoreHtml = `<p><b>No more CAPTCHAs</b></p><p>"""
|
|
1337
|
+
+ back_button_html
|
|
1338
|
+
+ r"""</p>`;
|
|
1257
1339
|
|
|
1258
1340
|
function handleToken(token) {
|
|
1259
1341
|
document.getElementById("puzzle-captcha").remove();
|
|
@@ -1279,12 +1361,14 @@ def setup_captcha_routes(app):
|
|
|
1279
1361
|
},
|
|
1280
1362
|
body: JSON.stringify({
|
|
1281
1363
|
token: token,
|
|
1282
|
-
|
|
1364
|
+
"""
|
|
1365
|
+
+ f"""package_id: '{package_id}',
|
|
1283
1366
|
title: '{js_single_quoted_string_safe(title)}',
|
|
1284
1367
|
link: link,
|
|
1285
1368
|
password: '{password}',
|
|
1286
1369
|
mirror: '{desired_mirror}',
|
|
1287
|
-
|
|
1370
|
+
"""
|
|
1371
|
+
+ """})
|
|
1288
1372
|
})
|
|
1289
1373
|
.then(response => response.json())
|
|
1290
1374
|
.then(data => {
|
|
@@ -1314,7 +1398,9 @@ def setup_captcha_routes(app):
|
|
|
1314
1398
|
reloadSection.style.display = "block";
|
|
1315
1399
|
});
|
|
1316
1400
|
}
|
|
1317
|
-
|
|
1401
|
+
"""
|
|
1402
|
+
+ obfuscated.cutcaptcha_custom_js()
|
|
1403
|
+
+ f'''</script>
|
|
1318
1404
|
<div>
|
|
1319
1405
|
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
1320
1406
|
<div id="package-selector-section">
|
|
@@ -1333,7 +1419,9 @@ def setup_captcha_routes(app):
|
|
|
1333
1419
|
</div>
|
|
1334
1420
|
<br>
|
|
1335
1421
|
<div id="delete-package-section">
|
|
1336
|
-
'''
|
|
1422
|
+
'''
|
|
1423
|
+
+ source_button_html
|
|
1424
|
+
+ f"""
|
|
1337
1425
|
<p>
|
|
1338
1426
|
{render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
|
|
1339
1427
|
</p>
|
|
@@ -1347,42 +1435,47 @@ def setup_captcha_routes(app):
|
|
|
1347
1435
|
{bypass_section}
|
|
1348
1436
|
</div>
|
|
1349
1437
|
</div>
|
|
1350
|
-
</html>
|
|
1438
|
+
</html>"""
|
|
1439
|
+
)
|
|
1351
1440
|
|
|
1352
1441
|
return content
|
|
1353
1442
|
|
|
1354
|
-
@app.post(
|
|
1443
|
+
@app.post("/captcha/<captcha_id>.html")
|
|
1355
1444
|
def proxy_html(captcha_id):
|
|
1356
|
-
target_url = f"{obfuscated.captcha_values()[
|
|
1445
|
+
target_url = f"{obfuscated.captcha_values()['url']}/captcha/{captcha_id}.html"
|
|
1357
1446
|
|
|
1358
|
-
headers = {
|
|
1447
|
+
headers = {
|
|
1448
|
+
key: value for key, value in request.headers.items() if key != "Host"
|
|
1449
|
+
}
|
|
1359
1450
|
data = request.body.read()
|
|
1360
1451
|
resp = requests.post(target_url, headers=headers, data=data, verify=False)
|
|
1361
1452
|
|
|
1362
|
-
response.content_type = resp.headers.get(
|
|
1453
|
+
response.content_type = resp.headers.get("Content-Type")
|
|
1363
1454
|
|
|
1364
1455
|
content = resp.text
|
|
1365
1456
|
content = re.sub(
|
|
1366
|
-
r
|
|
1367
|
-
r
|
|
1368
|
-
content
|
|
1457
|
+
r"""<script\s+src="/(jquery(?:-ui|\.ui\.touch-punch\.min)?\.js)(?:\?[^"]*)?"></script>""",
|
|
1458
|
+
r"""<script src="/captcha/js/\1"></script>""",
|
|
1459
|
+
content,
|
|
1369
1460
|
)
|
|
1370
1461
|
|
|
1371
|
-
response.content_type =
|
|
1462
|
+
response.content_type = "text/html"
|
|
1372
1463
|
return content
|
|
1373
1464
|
|
|
1374
|
-
@app.post(
|
|
1465
|
+
@app.post("/captcha/<captcha_id>.json")
|
|
1375
1466
|
def proxy_json(captcha_id):
|
|
1376
|
-
target_url = f"{obfuscated.captcha_values()[
|
|
1467
|
+
target_url = f"{obfuscated.captcha_values()['url']}/captcha/{captcha_id}.json"
|
|
1377
1468
|
|
|
1378
|
-
headers = {
|
|
1469
|
+
headers = {
|
|
1470
|
+
key: value for key, value in request.headers.items() if key != "Host"
|
|
1471
|
+
}
|
|
1379
1472
|
data = request.body.read()
|
|
1380
1473
|
resp = requests.post(target_url, headers=headers, data=data, verify=False)
|
|
1381
1474
|
|
|
1382
|
-
response.content_type = resp.headers.get(
|
|
1475
|
+
response.content_type = resp.headers.get("Content-Type")
|
|
1383
1476
|
return resp.content
|
|
1384
1477
|
|
|
1385
|
-
@app.get(
|
|
1478
|
+
@app.get("/captcha/js/<filename>")
|
|
1386
1479
|
def serve_local_js(filename):
|
|
1387
1480
|
upstream = f"{obfuscated.captcha_values()['url']}/{filename}"
|
|
1388
1481
|
try:
|
|
@@ -1392,27 +1485,27 @@ def setup_captcha_routes(app):
|
|
|
1392
1485
|
response.status = 502
|
|
1393
1486
|
return f"/* Error proxying {filename}: {e} */"
|
|
1394
1487
|
|
|
1395
|
-
response.content_type =
|
|
1488
|
+
response.content_type = "application/javascript"
|
|
1396
1489
|
return upstream_resp.iter_content(chunk_size=8192)
|
|
1397
1490
|
|
|
1398
|
-
@app.get(
|
|
1491
|
+
@app.get("/captcha/<captcha_id>/<uuid>/<filename>")
|
|
1399
1492
|
def proxy_pngs(captcha_id, uuid, filename):
|
|
1400
|
-
new_url = f"{obfuscated.captcha_values()[
|
|
1493
|
+
new_url = f"{obfuscated.captcha_values()['url']}/captcha/{captcha_id}/{uuid}/{filename}"
|
|
1401
1494
|
|
|
1402
1495
|
try:
|
|
1403
1496
|
external_response = requests.get(new_url, stream=True, verify=False)
|
|
1404
1497
|
external_response.raise_for_status()
|
|
1405
|
-
response.content_type =
|
|
1406
|
-
response.headers[
|
|
1498
|
+
response.content_type = "image/png"
|
|
1499
|
+
response.headers["Content-Disposition"] = f'inline; filename="{filename}"'
|
|
1407
1500
|
return external_response.iter_content(chunk_size=8192)
|
|
1408
1501
|
|
|
1409
1502
|
except requests.RequestException as e:
|
|
1410
1503
|
response.status = 502
|
|
1411
1504
|
return f"Error fetching resource: {e}"
|
|
1412
1505
|
|
|
1413
|
-
@app.post(
|
|
1506
|
+
@app.post("/captcha/<captcha_id>/check")
|
|
1414
1507
|
def proxy_check(captcha_id):
|
|
1415
|
-
new_url = f"{obfuscated.captcha_values()[
|
|
1508
|
+
new_url = f"{obfuscated.captcha_values()['url']}/captcha/{captcha_id}/check"
|
|
1416
1509
|
headers = {key: value for key, value in request.headers.items()}
|
|
1417
1510
|
|
|
1418
1511
|
data = request.body.read()
|
|
@@ -1420,19 +1513,24 @@ def setup_captcha_routes(app):
|
|
|
1420
1513
|
|
|
1421
1514
|
response.status = resp.status_code
|
|
1422
1515
|
for header in resp.headers:
|
|
1423
|
-
if header.lower() not in [
|
|
1516
|
+
if header.lower() not in [
|
|
1517
|
+
"content-encoding",
|
|
1518
|
+
"transfer-encoding",
|
|
1519
|
+
"content-length",
|
|
1520
|
+
"connection",
|
|
1521
|
+
]:
|
|
1424
1522
|
response.set_header(header, resp.headers[header])
|
|
1425
1523
|
return resp.content
|
|
1426
1524
|
|
|
1427
|
-
@app.post(
|
|
1525
|
+
@app.post("/captcha/bypass-submit")
|
|
1428
1526
|
def handle_bypass_submit():
|
|
1429
1527
|
"""Handle bypass submission with either links or DLC file"""
|
|
1430
1528
|
try:
|
|
1431
|
-
package_id = request.forms.get(
|
|
1432
|
-
title = request.forms.get(
|
|
1433
|
-
password = request.forms.get(
|
|
1434
|
-
links_input = request.forms.get(
|
|
1435
|
-
dlc_upload = request.files.get(
|
|
1529
|
+
package_id = request.forms.get("package_id")
|
|
1530
|
+
title = request.forms.get("title")
|
|
1531
|
+
password = request.forms.get("password", "")
|
|
1532
|
+
links_input = request.forms.get("links", "").strip()
|
|
1533
|
+
dlc_upload = request.files.get("dlc_file")
|
|
1436
1534
|
|
|
1437
1535
|
if not package_id or not title:
|
|
1438
1536
|
return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
@@ -1446,11 +1544,19 @@ def setup_captcha_routes(app):
|
|
|
1446
1544
|
# Process links input
|
|
1447
1545
|
if links_input:
|
|
1448
1546
|
info(f"Processing direct links bypass for {title}")
|
|
1449
|
-
raw_links = [
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1547
|
+
raw_links = [
|
|
1548
|
+
link.strip() for link in links_input.split("\n") if link.strip()
|
|
1549
|
+
]
|
|
1550
|
+
links = [
|
|
1551
|
+
l
|
|
1552
|
+
for l in raw_links
|
|
1553
|
+
if l.lower().startswith(("http://", "https://"))
|
|
1554
|
+
]
|
|
1555
|
+
|
|
1556
|
+
info(
|
|
1557
|
+
f"Received {len(links)} valid direct download links "
|
|
1558
|
+
f"(from {len(raw_links)} provided)"
|
|
1559
|
+
)
|
|
1454
1560
|
|
|
1455
1561
|
# Process DLC file
|
|
1456
1562
|
elif dlc_upload:
|
|
@@ -1479,20 +1585,28 @@ def setup_captcha_routes(app):
|
|
|
1479
1585
|
|
|
1480
1586
|
# Download the package
|
|
1481
1587
|
if links:
|
|
1482
|
-
downloaded = shared_state.download_package(
|
|
1588
|
+
downloaded = shared_state.download_package(
|
|
1589
|
+
links, title, password, package_id
|
|
1590
|
+
)
|
|
1483
1591
|
if downloaded:
|
|
1484
1592
|
StatsHelper(shared_state).increment_package_with_links(links)
|
|
1485
1593
|
StatsHelper(shared_state).increment_captcha_decryptions_manual()
|
|
1486
1594
|
shared_state.get_db("protected").delete(package_id)
|
|
1487
1595
|
|
|
1488
1596
|
# Check if there are more CAPTCHAs to solve
|
|
1489
|
-
remaining_protected = shared_state.get_db(
|
|
1597
|
+
remaining_protected = shared_state.get_db(
|
|
1598
|
+
"protected"
|
|
1599
|
+
).retrieve_all_titles()
|
|
1490
1600
|
has_more_captchas = bool(remaining_protected)
|
|
1491
1601
|
|
|
1492
1602
|
if has_more_captchas:
|
|
1493
|
-
solve_button = render_button(
|
|
1494
|
-
"
|
|
1495
|
-
|
|
1603
|
+
solve_button = render_button(
|
|
1604
|
+
"Solve another CAPTCHA",
|
|
1605
|
+
"primary",
|
|
1606
|
+
{
|
|
1607
|
+
"onclick": "location.href='/captcha'",
|
|
1608
|
+
},
|
|
1609
|
+
)
|
|
1496
1610
|
else:
|
|
1497
1611
|
solve_button = "<b>No more CAPTCHAs</b>"
|
|
1498
1612
|
|
|
@@ -1528,33 +1642,40 @@ def setup_captcha_routes(app):
|
|
|
1528
1642
|
{render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
|
|
1529
1643
|
</p>''')
|
|
1530
1644
|
|
|
1531
|
-
@app.post(
|
|
1645
|
+
@app.post("/captcha/decrypt-filecrypt")
|
|
1532
1646
|
def submit_token():
|
|
1533
1647
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
1534
1648
|
if not protected:
|
|
1535
|
-
return {
|
|
1649
|
+
return {
|
|
1650
|
+
"success": False,
|
|
1651
|
+
"title": "No protected packages found! CAPTCHA not needed.",
|
|
1652
|
+
}
|
|
1536
1653
|
|
|
1537
1654
|
links = []
|
|
1538
1655
|
title = "Unknown Package"
|
|
1539
1656
|
try:
|
|
1540
1657
|
data = request.json
|
|
1541
|
-
token = data.get(
|
|
1542
|
-
package_id = data.get(
|
|
1543
|
-
title = data.get(
|
|
1544
|
-
link = data.get(
|
|
1545
|
-
password = data.get(
|
|
1546
|
-
mirror = None if (mirror := data.get(
|
|
1658
|
+
token = data.get("token")
|
|
1659
|
+
package_id = data.get("package_id")
|
|
1660
|
+
title = data.get("title")
|
|
1661
|
+
link = data.get("link")
|
|
1662
|
+
password = data.get("password")
|
|
1663
|
+
mirror = None if (mirror := data.get("mirror")) == "None" else mirror
|
|
1547
1664
|
|
|
1548
1665
|
if token:
|
|
1549
1666
|
info(f"Received token: {token}")
|
|
1550
1667
|
info(f"Decrypting links for {title}")
|
|
1551
|
-
decrypted = get_filecrypt_links(
|
|
1668
|
+
decrypted = get_filecrypt_links(
|
|
1669
|
+
shared_state, token, title, link, password=password, mirror=mirror
|
|
1670
|
+
)
|
|
1552
1671
|
if decrypted:
|
|
1553
1672
|
links = decrypted.get("links", [])
|
|
1554
1673
|
info(f"Decrypted {len(links)} download links for {title}")
|
|
1555
1674
|
if not links:
|
|
1556
1675
|
raise ValueError("No download links found after decryption")
|
|
1557
|
-
downloaded = shared_state.download_package(
|
|
1676
|
+
downloaded = shared_state.download_package(
|
|
1677
|
+
links, title, password, package_id
|
|
1678
|
+
)
|
|
1558
1679
|
if downloaded:
|
|
1559
1680
|
StatsHelper(shared_state).increment_package_with_links(links)
|
|
1560
1681
|
shared_state.get_db("protected").delete(package_id)
|
|
@@ -1577,4 +1698,8 @@ def setup_captcha_routes(app):
|
|
|
1577
1698
|
remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
1578
1699
|
has_more_captchas = bool(remaining_protected)
|
|
1579
1700
|
|
|
1580
|
-
return {
|
|
1701
|
+
return {
|
|
1702
|
+
"success": success,
|
|
1703
|
+
"title": title,
|
|
1704
|
+
"has_more_captchas": has_more_captchas,
|
|
1705
|
+
}
|