quasarr 1.28.2__py3-none-any.whl → 1.30.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.

@@ -72,25 +72,17 @@ def setup_captcha_routes(app):
72
72
  except KeyError:
73
73
  desired_mirror = None
74
74
 
75
- # This is set for circle CAPTCHAs
76
- filecrypt_session = data.get("session", None)
77
-
78
75
  # This is required for cutcaptcha
79
76
  rapid = [ln for ln in links if "rapidgator" in ln[1].lower()]
80
77
  others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
81
78
  prioritized_links = rapid + others
82
79
 
83
- # This is required for bypass on circlecaptcha
84
- original_url = data.get("original_url", "")
85
-
86
80
  payload = {
87
81
  "package_id": package_id,
88
82
  "title": title,
89
83
  "password": password,
90
84
  "mirror": desired_mirror,
91
- "session": filecrypt_session,
92
85
  "links": prioritized_links,
93
- "original_url": original_url
94
86
  }
95
87
 
96
88
  encoded_payload = urlsafe_b64encode(json.dumps(payload).encode()).decode()
@@ -138,9 +130,6 @@ def setup_captcha_routes(app):
138
130
  elif has_tolink_links:
139
131
  debug("Redirecting to ToLink CAPTCHA")
140
132
  redirect(f"/captcha/tolink?data={quote(encoded_payload)}")
141
- elif filecrypt_session:
142
- debug(f'Redirecting to circle CAPTCHA')
143
- redirect(f"/captcha/circle?data={quote(encoded_payload)}")
144
133
  else:
145
134
  debug(f"Redirecting to cutcaptcha")
146
135
  redirect(f"/captcha/cutcaptcha?data={quote(encoded_payload)}")
@@ -230,7 +219,7 @@ def setup_captcha_routes(app):
230
219
 
231
220
  <!-- Primary action - the quick transfer link -->
232
221
  <p>
233
- {render_button(f"Open {provider_name} & Get Download Links", "primary", {"onclick": f"location.href='{url_with_quick_transfer_params}'"})}
222
+ {render_button(f"Open {provider_name} & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
234
223
  </p>
235
224
 
236
225
  <!-- Manual submission - collapsible -->
@@ -241,7 +230,7 @@ def setup_captcha_routes(app):
241
230
  <p style="font-size: 0.9em;">
242
231
  If the userscript doesn't work, you can manually paste the links below:
243
232
  </p>
244
- <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
233
+ <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data" onsubmit="if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();">
245
234
  <input type="hidden" name="package_id" value="{package_id}" />
246
235
  <input type="hidden" name="title" value="{title}" />
247
236
  <input type="hidden" name="password" value="{password}" />
@@ -319,11 +308,16 @@ def setup_captcha_routes(app):
319
308
 
320
309
  check_package_exists(package_id)
321
310
 
311
+ package_selector = render_package_selector(package_id)
312
+ failed_warning = render_failed_attempts_warning(package_id)
313
+
322
314
  return render_centered_html(f"""
323
315
  <!DOCTYPE html>
324
316
  <html>
325
317
  <body>
326
318
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
319
+ {package_selector}
320
+ {failed_warning}
327
321
  <p><b>Package:</b> {title}</p>
328
322
  {render_userscript_section(url, package_id, title, password, "hide")}
329
323
  <p>
@@ -355,11 +349,16 @@ def setup_captcha_routes(app):
355
349
 
356
350
  check_package_exists(package_id)
357
351
 
352
+ package_selector = render_package_selector(package_id)
353
+ failed_warning = render_failed_attempts_warning(package_id)
354
+
358
355
  return render_centered_html(f"""
359
356
  <!DOCTYPE html>
360
357
  <html>
361
358
  <body>
362
359
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
360
+ {package_selector}
361
+ {failed_warning}
363
362
  <p><b>Package:</b> {title}</p>
364
363
  {render_userscript_section(url, package_id, title, password, "junkies")}
365
364
  <p>
@@ -392,11 +391,16 @@ def setup_captcha_routes(app):
392
391
 
393
392
  url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
394
393
 
394
+ package_selector = render_package_selector(package_id)
395
+ failed_warning = render_failed_attempts_warning(package_id)
396
+
395
397
  return render_centered_html(f"""
396
398
  <!DOCTYPE html>
397
399
  <html>
398
400
  <body>
399
401
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
402
+ {package_selector}
403
+ {failed_warning}
400
404
  <p><b>Package:</b> {title}</p>
401
405
  {render_userscript_section(url, package_id, title, password, "keeplinks")}
402
406
  <p>
@@ -429,11 +433,16 @@ def setup_captcha_routes(app):
429
433
 
430
434
  url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
431
435
 
436
+ package_selector = render_package_selector(package_id)
437
+ failed_warning = render_failed_attempts_warning(package_id)
438
+
432
439
  return render_centered_html(f"""
433
440
  <!DOCTYPE html>
434
441
  <html>
435
442
  <body>
436
443
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
444
+ {package_selector}
445
+ {failed_warning}
437
446
  <p><b>Package:</b> {title}</p>
438
447
  {render_userscript_section(url, package_id, title, password, "tolink")}
439
448
  <p>
@@ -480,7 +489,7 @@ def setup_captcha_routes(app):
480
489
  return content
481
490
 
482
491
  def render_filecrypt_bypass_section(url, package_id, title, password):
483
- """Render the bypass UI section for both cutcaptcha and circle captcha pages"""
492
+ """Render the bypass UI section for cutcaptcha captcha page"""
484
493
 
485
494
  # Generate userscript URL with transfer params
486
495
  # Get base URL of current request
@@ -539,7 +548,7 @@ def setup_captcha_routes(app):
539
548
 
540
549
  <!-- Primary action button -->
541
550
  <p>
542
- {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"location.href='{url_with_quick_transfer_params}'"})}
551
+ {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
543
552
  </p>
544
553
 
545
554
  <!-- Manual submission section -->
@@ -547,7 +556,7 @@ def setup_captcha_routes(app):
547
556
  <p style="font-size: 0.9em; margin-bottom: 16px;">
548
557
  If the userscript doesn't work, you can manually paste the links or upload a DLC file:
549
558
  </p>
550
- <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
559
+ <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data" onsubmit="if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();">
551
560
  <input type="hidden" name="package_id" value="{package_id}" />
552
561
  <input type="hidden" name="title" value="{title}" />
553
562
  <input type="hidden" name="password" value="{password}" />
@@ -610,6 +619,339 @@ def setup_captcha_routes(app):
610
619
  </script>
611
620
  '''
612
621
 
622
+ def render_package_selector(current_package_id):
623
+ """Render a dropdown selector for all available packages at the top of captcha UIs"""
624
+ protected = shared_state.get_db("protected").retrieve_all_titles()
625
+
626
+ if not protected or len(protected) <= 1:
627
+ return "" # Don't show selector if only one or no packages
628
+
629
+ sj = shared_state.values["config"]("Hostnames").get("sj")
630
+ dj = shared_state.values["config"]("Hostnames").get("dj")
631
+
632
+ def is_junkies_link(link):
633
+ url = link[0] if isinstance(link, (list, tuple)) else link
634
+ mirror = link[1] if isinstance(link, (list, tuple)) and len(link) > 1 else ""
635
+ if mirror == "junkies":
636
+ return True
637
+ return (sj and sj in url) or (dj and dj in url)
638
+
639
+ def get_captcha_type_for_links(links):
640
+ """Determine which captcha type to use based on links"""
641
+ has_hide = any(("hide." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
642
+ has_junkies = any(is_junkies_link(l) for l in links)
643
+ has_keeplinks = any(("keeplinks." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
644
+ has_tolink = any(("tolink." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
645
+
646
+ if has_hide:
647
+ return "hide"
648
+ elif has_junkies:
649
+ return "junkies"
650
+ elif has_keeplinks:
651
+ return "keeplinks"
652
+ elif has_tolink:
653
+ return "tolink"
654
+ else:
655
+ return "cutcaptcha"
656
+
657
+ options = []
658
+ for package in protected:
659
+ pkg_id = package[0]
660
+ data = json.loads(package[1])
661
+ title = data.get("title", "Unknown")
662
+ links = data.get("links", [])
663
+ password = data.get("password", "")
664
+ mirror = data.get("mirror")
665
+
666
+ # Prioritize rapidgator links for cutcaptcha
667
+ rapid = [ln for ln in links if "rapidgator" in ln[1].lower()]
668
+ others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
669
+ prioritized = rapid + others
670
+
671
+ payload = {
672
+ "package_id": pkg_id,
673
+ "title": title,
674
+ "password": password,
675
+ "mirror": mirror,
676
+ "links": prioritized,
677
+ }
678
+ encoded = urlsafe_b64encode(json.dumps(payload).encode()).decode()
679
+ captcha_type = get_captcha_type_for_links(prioritized)
680
+
681
+ selected = "selected" if pkg_id == current_package_id else ""
682
+ # Truncate long titles for display
683
+ display_title = (title[:50] + "...") if len(title) > 53 else title
684
+ options.append(f'<option value="{captcha_type}|{quote(encoded)}" {selected}>{display_title}</option>')
685
+
686
+ options_html = "\n".join(options)
687
+
688
+ return f'''
689
+ <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;">
690
+ <label for="package-select" style="display: block; margin-bottom: 8px; font-weight: bold;">📦 Select Package:</label>
691
+ <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;">
692
+ {options_html}
693
+ </select>
694
+ </div>
695
+ <script>
696
+ document.getElementById('package-select').addEventListener('change', function() {{
697
+ const [captchaType, encodedData] = this.value.split('|');
698
+ window.location.href = '/captcha/' + captchaType + '?data=' + encodedData;
699
+ }});
700
+ </script>
701
+ '''
702
+
703
+ def render_failed_attempts_warning(package_id, include_delete_button=True, fallback_url=None):
704
+ """Render a warning block that shows after 2+ failed attempts per package_id.
705
+ Uses localStorage to track attempts by package_id to ensure reliable tracking
706
+ even when package titles are duplicated.
707
+
708
+ Attempts are NOT incremented on page load - they must be incremented by
709
+ calling window.incrementCaptchaAttempts() when user takes an action (e.g.,
710
+ clicking submit, opening bypass link).
711
+
712
+ Args:
713
+ package_id: The unique package identifier
714
+ include_delete_button: Whether to show delete button in warning
715
+ fallback_url: Optional URL to a fallback page (e.g., FileCrypt manual fallback)
716
+ """
717
+
718
+ delete_button = ""
719
+ if include_delete_button:
720
+ delete_button = render_button("Delete Package", "primary",
721
+ {"onclick": f"location.href='/captcha/delete/{package_id}'"})
722
+
723
+ fallback_link = ""
724
+ if fallback_url:
725
+ fallback_link = f'''
726
+ <p style="margin-top: 12px; margin-bottom: 8px;">
727
+ <a href="{fallback_url}" style="color: #cc0000;">Try the manual FileCrypt fallback page →</a>
728
+ </p>
729
+ '''
730
+
731
+ return f'''
732
+ <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;">
733
+ <h3 style="color: #dc2626; margin-top: 0;">⚠️ Multiple Failed Attempts Detected</h3>
734
+ <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>
735
+ <p style="margin-bottom: 8px; color: #7f1d1d;">Please verify the link is still valid, or delete this package if it's no longer available.</p>
736
+ {fallback_link}
737
+ <div id="warning-delete-button" style="margin-top: 12px;">
738
+ {delete_button}
739
+ </div>
740
+ </div>
741
+ <script>
742
+ (function() {{
743
+ const packageId = '{package_id}';
744
+ const storageKey = 'captcha_attempts_' + packageId;
745
+
746
+ // Get current attempt count (do NOT increment on page load)
747
+ let attempts = parseInt(localStorage.getItem(storageKey) || '0', 10);
748
+
749
+ // Show warning if 2+ failed attempts
750
+ if (attempts >= 2) {{
751
+ const warningBox = document.getElementById('failed-attempts-warning');
752
+ if (warningBox) {{
753
+ warningBox.style.display = 'block';
754
+ }}
755
+ }}
756
+
757
+ // Function to increment attempts (call this on submit/action)
758
+ window.incrementCaptchaAttempts = function() {{
759
+ let current = parseInt(localStorage.getItem(storageKey) || '0', 10);
760
+ current++;
761
+ localStorage.setItem(storageKey, current.toString());
762
+ // Show warning immediately if we hit 2+ attempts
763
+ if (current >= 2) {{
764
+ const warningBox = document.getElementById('failed-attempts-warning');
765
+ if (warningBox) {{
766
+ warningBox.style.display = 'block';
767
+ }}
768
+ }}
769
+ return current;
770
+ }};
771
+
772
+ // Function to get current attempt count
773
+ window.getCaptchaAttempts = function() {{
774
+ return parseInt(localStorage.getItem(storageKey) || '0', 10);
775
+ }};
776
+
777
+ // Function to clear attempts (call on success)
778
+ window.clearCaptchaAttempts = function() {{
779
+ localStorage.removeItem(storageKey);
780
+ }};
781
+ }})();
782
+ </script>
783
+ '''
784
+
785
+ @app.get("/captcha/filecrypt")
786
+ def serve_filecrypt_fallback():
787
+ """Dedicated FileCrypt fallback page - similar to hide/junkies/keeplinks/tolink"""
788
+ payload = decode_payload()
789
+
790
+ if "error" in payload:
791
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
792
+ <p>{payload["error"]}</p>
793
+ <p>
794
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
795
+ </p>''')
796
+
797
+ package_id = payload.get("package_id")
798
+ title = payload.get("title")
799
+ password = payload.get("password")
800
+ urls = payload.get("links")
801
+
802
+ check_package_exists(package_id)
803
+
804
+ url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
805
+
806
+ # Generate userscript URL with transfer params
807
+ base_url = request.urlparts.scheme + '://' + request.urlparts.netloc
808
+ transfer_url = f"{base_url}/captcha/quick-transfer"
809
+
810
+ url_with_quick_transfer_params = (
811
+ f"{url}?"
812
+ f"transfer_url={quote(transfer_url)}&"
813
+ f"pkg_id={quote(package_id)}&"
814
+ f"pkg_title={quote(title)}&"
815
+ f"pkg_pass={quote(password)}"
816
+ )
817
+
818
+ package_selector = render_package_selector(package_id)
819
+ failed_warning = render_failed_attempts_warning(package_id)
820
+
821
+ return render_centered_html(f"""
822
+ <!DOCTYPE html>
823
+ <html>
824
+ <body>
825
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
826
+ {package_selector}
827
+ {failed_warning}
828
+ <p style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
829
+
830
+ <div>
831
+ <!-- Info section explaining the process -->
832
+ <div class="info-box">
833
+ <h3>ℹ️ How This Works:</h3>
834
+ <p style="margin-bottom: 8px;">
835
+ 1. Click the button below to open FileCrypt directly
836
+ </p>
837
+ <p style="margin-top: 0; margin-bottom: 8px;">
838
+ 2. Solve any CAPTCHAs on their site to reveal the download links
839
+ </p>
840
+ <p style="margin-top: 0; margin-bottom: 0;">
841
+ 3. <b>With the userscript installed</b>, links are automatically sent back to Quasarr!
842
+ </p>
843
+ </div>
844
+
845
+ <!-- One-time setup section - visually separated -->
846
+ <div id="setup-instructions" class="setup-box">
847
+ <h3>📦 First Time Setup:</h3>
848
+ <p style="margin-bottom: 8px;">
849
+ <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. Install Tampermonkey</a>
850
+ </p>
851
+ <p style="margin-top: 0; margin-bottom: 12px;">
852
+ <a href="/captcha/filecrypt.user.js" target="_blank">2. Install the FileCrypt userscript</a>
853
+ </p>
854
+ <p style="margin-top: 0;">
855
+ <button id="hide-setup-btn" type="button" class="btn-subtle">
856
+ ✅ Don't show this again
857
+ </button>
858
+ </p>
859
+ </div>
860
+
861
+ <!-- Hidden "show instructions" button -->
862
+ <div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
863
+ <button id="show-setup-btn" type="button" class="btn-subtle">
864
+ ℹ️ Show setup instructions
865
+ </button>
866
+ </div>
867
+
868
+ <!-- Primary action button -->
869
+ <p>
870
+ {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
871
+ </p>
872
+
873
+ <!-- Manual submission section -->
874
+ <div class="section-divider">
875
+ <details id="manualSubmitDetails">
876
+ <summary id="manualSubmitSummary" style="cursor: pointer;">Show Manual Submission</summary>
877
+ <div style="margin-top: 16px;">
878
+ <p style="font-size: 0.9em; margin-bottom: 16px;">
879
+ If the userscript doesn't work, you can manually paste the links or upload a DLC file:
880
+ </p>
881
+ <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data" onsubmit="if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();">
882
+ <input type="hidden" name="package_id" value="{package_id}" />
883
+ <input type="hidden" name="title" value="{title}" />
884
+ <input type="hidden" name="password" value="{password}" />
885
+
886
+ <div>
887
+ <strong>Paste the download links (one per line):</strong>
888
+ <textarea id="links-input" name="links" rows="5" style="width: 100%; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
889
+ </div>
890
+
891
+ <div>
892
+ <strong>Or upload DLC file:</strong><br>
893
+ <input type="file" id="dlc-file" name="dlc_file" accept=".dlc" />
894
+ </div>
895
+
896
+ <div>
897
+ {render_button("Submit", "primary", {"type": "submit"})}
898
+ </div>
899
+ </form>
900
+ </div>
901
+ </details>
902
+ </div>
903
+ </div>
904
+
905
+ <p>
906
+ {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
907
+ </p>
908
+ <p>
909
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
910
+ </p>
911
+
912
+ <script>
913
+ // Handle manual submission toggle text
914
+ const manualDetails = document.getElementById('manualSubmitDetails');
915
+ const manualSummary = document.getElementById('manualSubmitSummary');
916
+
917
+ if (manualDetails && manualSummary) {{
918
+ manualDetails.addEventListener('toggle', () => {{
919
+ if (manualDetails.open) {{
920
+ manualSummary.textContent = 'Hide Manual Submission';
921
+ }} else {{
922
+ manualSummary.textContent = 'Show Manual Submission';
923
+ }}
924
+ }});
925
+ }}
926
+
927
+ // Handle setup instructions hide/show
928
+ const hideSetup = localStorage.getItem('hideFileCryptFallbackSetupInstructions');
929
+ const setupBox = document.getElementById('setup-instructions');
930
+ const showLink = document.getElementById('show-instructions-link');
931
+
932
+ if (hideSetup === 'true') {{
933
+ setupBox.style.display = 'none';
934
+ showLink.style.display = 'block';
935
+ }}
936
+
937
+ // Hide setup instructions
938
+ document.getElementById('hide-setup-btn').addEventListener('click', function() {{
939
+ localStorage.setItem('hideFileCryptFallbackSetupInstructions', 'true');
940
+ setupBox.style.display = 'none';
941
+ showLink.style.display = 'block';
942
+ }});
943
+
944
+ // Show setup instructions again
945
+ document.getElementById('show-setup-btn').addEventListener('click', function() {{
946
+ localStorage.setItem('hideFileCryptFallbackSetupInstructions', 'false');
947
+ setupBox.style.display = 'block';
948
+ showLink.style.display = 'none';
949
+ }});
950
+ </script>
951
+
952
+ </body>
953
+ </html>""")
954
+
613
955
  @app.get('/captcha/quick-transfer')
614
956
  def handle_quick_transfer():
615
957
  """Handle quick transfer from userscript"""
@@ -711,7 +1053,8 @@ def setup_captcha_routes(app):
711
1053
  </p>
712
1054
  <p>
713
1055
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
714
- </p>''')
1056
+ </p>
1057
+ <script>localStorage.removeItem('captcha_attempts_{package_id}');</script>''')
715
1058
  else:
716
1059
  StatsHelper(shared_state).increment_failed_decryptions_manual()
717
1060
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
@@ -751,7 +1094,8 @@ def setup_captcha_routes(app):
751
1094
  </p>
752
1095
  <p>
753
1096
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
754
- </p>''')
1097
+ </p>
1098
+ <script>localStorage.removeItem('captcha_attempts_{package_id}');</script>''')
755
1099
  else:
756
1100
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
757
1101
  <p>Failed to delete package!</p>
@@ -826,8 +1170,50 @@ def setup_captcha_routes(app):
826
1170
  # Add bypass section
827
1171
  bypass_section = render_filecrypt_bypass_section(url, package_id, title, password)
828
1172
 
1173
+ # Add package selector and failed attempts warning
1174
+ package_selector = render_package_selector(package_id)
1175
+
1176
+ # Create fallback URL for the manual FileCrypt page
1177
+ fallback_payload = {
1178
+ "package_id": package_id,
1179
+ "title": title,
1180
+ "password": password,
1181
+ "mirror": desired_mirror,
1182
+ "links": prioritized_links,
1183
+ }
1184
+ fallback_encoded = urlsafe_b64encode(json.dumps(fallback_payload).encode()).decode()
1185
+ filecrypt_fallback_url = f"/captcha/filecrypt?data={quote(fallback_encoded)}"
1186
+
1187
+ failed_warning = render_failed_attempts_warning(package_id, include_delete_button=False,
1188
+ fallback_url=filecrypt_fallback_url) # Delete button is already below
1189
+
829
1190
  content = render_centered_html(r'''
1191
+ <style>
1192
+ @media (max-width: 600px) {
1193
+ .package-selector,
1194
+ #failed-attempts-warning {
1195
+ margin-left: 0 !important;
1196
+ margin-right: 0 !important;
1197
+ padding-left: 8px !important;
1198
+ padding-right: 8px !important;
1199
+ border-radius: 0 !important;
1200
+ border-left: none !important;
1201
+ border-right: none !important;
1202
+ }
1203
+ }
1204
+ </style>
830
1205
  <script type="text/javascript">
1206
+ // Check if we should redirect to fallback due to failed attempts
1207
+ (function() {
1208
+ const storageKey = 'captcha_attempts_''' + package_id + r'''';
1209
+ const attempts = parseInt(localStorage.getItem(storageKey) || '0', 10);
1210
+ if (attempts >= 2) {
1211
+ // Redirect to FileCrypt fallback page
1212
+ window.location.href = '''' + filecrypt_fallback_url + r'''';
1213
+ return;
1214
+ }
1215
+ })();
1216
+
831
1217
  var api_key = "''' + obfuscated.captcha_values()["api_key"] + r'''";
832
1218
  var endpoint = '/' + window.location.pathname.split('/')[1] + '/' + api_key + '.html';
833
1219
  var solveAnotherHtml = `<p>''' + solve_another_html + r'''</p><p>''' + back_button_html + r'''</p>`;
@@ -839,6 +1225,11 @@ def setup_captcha_routes(app):
839
1225
  document.getElementById("delete-package-section").style.display = "none";
840
1226
  document.getElementById("back-button-section").style.display = "none";
841
1227
  document.getElementById("bypass-section").style.display = "none";
1228
+ // Hide package selector and warning on token submission
1229
+ var pkgSelector = document.getElementById("package-selector-section");
1230
+ if (pkgSelector) pkgSelector.style.display = "none";
1231
+ var warnBox = document.getElementById("failed-attempts-warning");
1232
+ if (warnBox) warnBox.style.display = "none";
842
1233
 
843
1234
  // Remove width limit on result screen
844
1235
  var packageTitle = document.getElementById("package-title");
@@ -867,9 +1258,17 @@ def setup_captcha_routes(app):
867
1258
  if (data.success) {
868
1259
  document.getElementById("captcha-key").insertAdjacentHTML('afterend',
869
1260
  '<p>✅ Successful!</p>');
1261
+ // Clear failed attempts on success
1262
+ if (typeof clearCaptchaAttempts === 'function') {
1263
+ clearCaptchaAttempts();
1264
+ }
870
1265
  } else {
871
1266
  document.getElementById("captcha-key").insertAdjacentHTML('afterend',
872
1267
  '<p>Failed. Check console for details!</p>');
1268
+ // Increment failed attempts on failure
1269
+ if (typeof incrementCaptchaAttempts === 'function') {
1270
+ incrementCaptchaAttempts();
1271
+ }
873
1272
  }
874
1273
 
875
1274
  // Show appropriate button based on whether more CAPTCHAs exist
@@ -885,6 +1284,10 @@ def setup_captcha_routes(app):
885
1284
  ''' + obfuscated.cutcaptcha_custom_js() + f'''</script>
886
1285
  <div>
887
1286
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
1287
+ <div id="package-selector-section">
1288
+ {package_selector}
1289
+ </div>
1290
+ {failed_warning}
888
1291
  <p id="package-title" style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
889
1292
  <div id="captcha-key"></div>
890
1293
  {link_select}<br><br>
@@ -1068,7 +1471,8 @@ def setup_captcha_routes(app):
1068
1471
  </p>
1069
1472
  <p>
1070
1473
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
1071
- </p>''')
1474
+ </p>
1475
+ <script>localStorage.removeItem('captcha_attempts_{package_id}');</script>''')
1072
1476
  else:
1073
1477
  StatsHelper(shared_state).increment_failed_decryptions_manual()
1074
1478
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
@@ -1113,38 +1517,17 @@ def setup_captcha_routes(app):
1113
1517
  info(f"Decrypting links for {title}")
1114
1518
  decrypted = get_filecrypt_links(shared_state, token, title, link, password=password, mirror=mirror)
1115
1519
  if decrypted:
1116
- if decrypted.get("status", "") == "replaced":
1117
- replace_url = decrypted.get("replace_url")
1118
- session = decrypted.get("session")
1119
- mirror = decrypted.get("mirror", "filecrypt")
1120
-
1121
- links = [replace_url]
1122
-
1123
- blob = json.dumps(
1124
- {
1125
- "title": title,
1126
- "links": [replace_url, mirror],
1127
- "size_mb": 0,
1128
- "password": password,
1129
- "mirror": mirror,
1130
- "session": session,
1131
- "original_url": link
1132
- })
1133
- shared_state.get_db("protected").update_store(package_id, blob)
1134
- info(f"Another CAPTCHA solution is required for {mirror} link: {replace_url}")
1135
-
1520
+ links = decrypted.get("links", [])
1521
+ info(f"Decrypted {len(links)} download links for {title}")
1522
+ if not links:
1523
+ raise ValueError("No download links found after decryption")
1524
+ downloaded = shared_state.download_package(links, title, password, package_id)
1525
+ if downloaded:
1526
+ StatsHelper(shared_state).increment_package_with_links(links)
1527
+ shared_state.get_db("protected").delete(package_id)
1136
1528
  else:
1137
- links = decrypted.get("links", [])
1138
- info(f"Decrypted {len(links)} download links for {title}")
1139
- if not links:
1140
- raise ValueError("No download links found after decryption")
1141
- downloaded = shared_state.download_package(links, title, password, package_id)
1142
- if downloaded:
1143
- StatsHelper(shared_state).increment_package_with_links(links)
1144
- shared_state.get_db("protected").delete(package_id)
1145
- else:
1146
- links = []
1147
- raise RuntimeError("Submitting Download to JDownloader failed")
1529
+ links = []
1530
+ raise RuntimeError("Submitting Download to JDownloader failed")
1148
1531
  else:
1149
1532
  raise ValueError("No download links found")
1150
1533
 
@@ -1162,171 +1545,3 @@ def setup_captcha_routes(app):
1162
1545
  has_more_captchas = bool(remaining_protected)
1163
1546
 
1164
1547
  return {"success": success, "title": title, "has_more_captchas": has_more_captchas}
1165
-
1166
- # The following routes are for circle CAPTCHA
1167
- @app.get('/captcha/circle')
1168
- def serve_circle():
1169
- payload = decode_payload()
1170
-
1171
- if "error" in payload:
1172
- return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
1173
- <p>{payload["error"]}</p>
1174
- <p>
1175
- {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
1176
- </p>''')
1177
-
1178
- package_id = payload.get("package_id")
1179
- session_id = payload.get("session")
1180
- title = payload.get("title", "Unknown Package")
1181
- password = payload.get("password", "")
1182
- original_url = payload.get("original_url", "")
1183
- url = payload.get("links")[0] if payload.get("links") else None
1184
-
1185
- check_package_exists(package_id)
1186
-
1187
- if not url or not session_id or not package_id:
1188
- response.status = 400
1189
- return "Missing required parameters"
1190
-
1191
- # Add bypass section
1192
- bypass_section = render_filecrypt_bypass_section(original_url, package_id, title, password)
1193
-
1194
- return render_centered_html(f"""
1195
- <!DOCTYPE html>
1196
- <html>
1197
- <body>
1198
- <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
1199
- <p><b>Package:</b> {title}</p>
1200
- <form action="/captcha/decrypt-filecrypt-circle?url={url}&session_id={session_id}&package_id={package_id}" method="post">
1201
- <input type="image" src="/captcha/circle.php?url={url}&session_id={session_id}" name="button" alt="Circle CAPTCHA">
1202
- </form>
1203
- <p>
1204
- {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
1205
- </p>
1206
- <p>
1207
- {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
1208
- </p>
1209
- {bypass_section}
1210
- </body>
1211
- </html>""")
1212
-
1213
- @app.get('/captcha/circle.php')
1214
- def proxy_circle_php():
1215
- target_url = "https://filecrypt.cc/captcha/circle.php"
1216
-
1217
- url = request.query.get('url')
1218
- session_id = request.query.get('session_id')
1219
- if not url or not session_id:
1220
- response.status = 400
1221
- return "Missing required parameters"
1222
-
1223
- headers = {'User-Agent': shared_state.values["user_agent"]}
1224
- cookies = {'PHPSESSID': session_id}
1225
- resp = requests.get(target_url, headers=headers, cookies=cookies, verify=False)
1226
-
1227
- response.content_type = resp.headers.get('Content-Type', 'application/octet-stream')
1228
- return resp.content
1229
-
1230
- @app.post('/captcha/decrypt-filecrypt-circle')
1231
- def proxy_form_submit():
1232
- url = request.query.get('url')
1233
- session_id = request.query.get('session_id')
1234
- package_id = request.query.get('package_id')
1235
- success = False
1236
-
1237
- if not url or not session_id or not package_id:
1238
- response.status = 400
1239
- return "Missing required parameters"
1240
-
1241
- cookies = {'PHPSESSID': session_id}
1242
-
1243
- headers = {
1244
- 'User-Agent': shared_state.values["user_agent"],
1245
- "Content-Type": "application/x-www-form-urlencoded"
1246
- }
1247
-
1248
- raw_body = request.body.read()
1249
-
1250
- resp = requests.post(url, cookies=cookies, headers=headers, data=raw_body, verify=False)
1251
- response.content_type = resp.headers.get('Content-Type', 'text/html')
1252
-
1253
- if "<h2>Security Check</h2>" in resp.text or "click inside the open circle" in resp.text:
1254
- status = "CAPTCHA verification failed. Please try again."
1255
- info(status)
1256
-
1257
- match = re.search(
1258
- r"top\.location\.href\s*=\s*['\"]([^'\"]*?/go\b[^'\"]*)['\"]",
1259
- resp.text,
1260
- re.IGNORECASE
1261
- )
1262
- if match:
1263
- redirect = match.group(1)
1264
- resolved_url = urljoin(url, redirect)
1265
- info(f"Redirect URL: {resolved_url}")
1266
- try:
1267
- redirect_resp = requests.post(resolved_url, cookies=cookies, headers=headers, allow_redirects=True,
1268
- timeout=10, verify=False)
1269
-
1270
- if "expired" in redirect_resp.text.lower():
1271
- status = f"The CAPTCHA session has expired. Deleting package: {package_id}"
1272
- info(status)
1273
- shared_state.get_db("protected").delete(package_id)
1274
- else:
1275
- download_link = redirect_resp.url
1276
- if redirect_resp.ok:
1277
- status = f"Successfully resolved download link!"
1278
- info(status)
1279
-
1280
- raw_data = shared_state.get_db("protected").retrieve(package_id)
1281
- data = json.loads(raw_data)
1282
- title = data.get("title")
1283
- password = data.get("password", "")
1284
- links = [download_link]
1285
- downloaded = shared_state.download_package(links, title, password, package_id)
1286
- if downloaded:
1287
- StatsHelper(shared_state).increment_package_with_links(links)
1288
- success = True
1289
- shared_state.get_db("protected").delete(package_id)
1290
- else:
1291
- raise RuntimeError("Submitting Download to JDownloader failed")
1292
- else:
1293
- info(
1294
- f"Failed to reach redirect target. Status: {redirect_resp.status_code}, Solution: {status}")
1295
- except Exception as e:
1296
- info(f"Error while resolving download link: {e}")
1297
- else:
1298
- if resp.url.endswith("404.html"):
1299
- info("Your IP has been blocked by Filecrypt. Please try again later.")
1300
- else:
1301
- info("You did not solve the CAPTCHA correctly. Please try again.")
1302
-
1303
- if success:
1304
- StatsHelper(shared_state).increment_captcha_decryptions_manual()
1305
- else:
1306
- StatsHelper(shared_state).increment_failed_decryptions_manual()
1307
-
1308
- # Check if there are more CAPTCHAs to solve
1309
- remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
1310
- has_more_captchas = bool(remaining_protected)
1311
-
1312
- if has_more_captchas:
1313
- solve_button = render_button("Solve another CAPTCHA", "primary", {
1314
- "onclick": "location.href='/captcha'",
1315
- })
1316
- else:
1317
- solve_button = "<b>No more CAPTCHAs</b>"
1318
-
1319
- return render_centered_html(f"""
1320
- <!DOCTYPE html>
1321
- <html>
1322
- <body>
1323
- <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
1324
- <p>{status}</p>
1325
- <p>
1326
- {solve_button}
1327
- </p>
1328
- <p>
1329
- {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
1330
- </p>
1331
- </body>
1332
- </html>""")