quasarr 1.29.0__py3-none-any.whl → 1.31.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.

@@ -5,7 +5,7 @@
5
5
  import json
6
6
  import re
7
7
  from base64 import urlsafe_b64encode, urlsafe_b64decode
8
- from urllib.parse import quote, unquote, urljoin
8
+ from urllib.parse import quote, unquote
9
9
 
10
10
  import requests
11
11
  from bottle import request, response, redirect, HTTPResponse
@@ -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,12 +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, title)
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>
327
- <p><b>Package:</b> {title}</p>
319
+ {package_selector}
320
+ {failed_warning}
328
321
  {render_userscript_section(url, package_id, title, password, "hide")}
329
322
  <p>
330
323
  {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
@@ -355,12 +348,16 @@ def setup_captcha_routes(app):
355
348
 
356
349
  check_package_exists(package_id)
357
350
 
351
+ package_selector = render_package_selector(package_id, title)
352
+ failed_warning = render_failed_attempts_warning(package_id)
353
+
358
354
  return render_centered_html(f"""
359
355
  <!DOCTYPE html>
360
356
  <html>
361
357
  <body>
362
358
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
363
- <p><b>Package:</b> {title}</p>
359
+ {package_selector}
360
+ {failed_warning}
364
361
  {render_userscript_section(url, package_id, title, password, "junkies")}
365
362
  <p>
366
363
  {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
@@ -392,12 +389,16 @@ def setup_captcha_routes(app):
392
389
 
393
390
  url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
394
391
 
392
+ package_selector = render_package_selector(package_id, title)
393
+ failed_warning = render_failed_attempts_warning(package_id)
394
+
395
395
  return render_centered_html(f"""
396
396
  <!DOCTYPE html>
397
397
  <html>
398
398
  <body>
399
399
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
400
- <p><b>Package:</b> {title}</p>
400
+ {package_selector}
401
+ {failed_warning}
401
402
  {render_userscript_section(url, package_id, title, password, "keeplinks")}
402
403
  <p>
403
404
  {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
@@ -429,12 +430,16 @@ def setup_captcha_routes(app):
429
430
 
430
431
  url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
431
432
 
433
+ package_selector = render_package_selector(package_id, title)
434
+ failed_warning = render_failed_attempts_warning(package_id)
435
+
432
436
  return render_centered_html(f"""
433
437
  <!DOCTYPE html>
434
438
  <html>
435
439
  <body>
436
440
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
437
- <p><b>Package:</b> {title}</p>
441
+ {package_selector}
442
+ {failed_warning}
438
443
  {render_userscript_section(url, package_id, title, password, "tolink")}
439
444
  <p>
440
445
  {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
@@ -480,7 +485,7 @@ def setup_captcha_routes(app):
480
485
  return content
481
486
 
482
487
  def render_filecrypt_bypass_section(url, package_id, title, password):
483
- """Render the bypass UI section for both cutcaptcha and circle captcha pages"""
488
+ """Render the bypass UI section for cutcaptcha captcha page"""
484
489
 
485
490
  # Generate userscript URL with transfer params
486
491
  # Get base URL of current request
@@ -539,7 +544,7 @@ def setup_captcha_routes(app):
539
544
 
540
545
  <!-- Primary action button -->
541
546
  <p>
542
- {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"location.href='{url_with_quick_transfer_params}'"})}
547
+ {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
543
548
  </p>
544
549
 
545
550
  <!-- Manual submission section -->
@@ -547,7 +552,7 @@ def setup_captcha_routes(app):
547
552
  <p style="font-size: 0.9em; margin-bottom: 16px;">
548
553
  If the userscript doesn't work, you can manually paste the links or upload a DLC file:
549
554
  </p>
550
- <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
555
+ <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data" onsubmit="if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();">
551
556
  <input type="hidden" name="package_id" value="{package_id}" />
552
557
  <input type="hidden" name="title" value="{title}" />
553
558
  <input type="hidden" name="password" value="{password}" />
@@ -610,6 +615,348 @@ def setup_captcha_routes(app):
610
615
  </script>
611
616
  '''
612
617
 
618
+ def render_package_selector(current_package_id, current_title=None):
619
+ """Render package title, with dropdown selector if multiple packages available"""
620
+ protected = shared_state.get_db("protected").retrieve_all_titles()
621
+
622
+ if not protected:
623
+ return ""
624
+
625
+ # Single package - just show the title without dropdown
626
+ if len(protected) <= 1:
627
+ if current_title:
628
+ return f'''
629
+ <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;">
630
+ <p style="margin: 0; word-break: break-all;"><b>📦 Package:</b> {current_title}</p>
631
+ </div>
632
+ '''
633
+ return ""
634
+
635
+ sj = shared_state.values["config"]("Hostnames").get("sj")
636
+ dj = shared_state.values["config"]("Hostnames").get("dj")
637
+
638
+ def is_junkies_link(link):
639
+ url = link[0] if isinstance(link, (list, tuple)) else link
640
+ mirror = link[1] if isinstance(link, (list, tuple)) and len(link) > 1 else ""
641
+ if mirror == "junkies":
642
+ return True
643
+ return (sj and sj in url) or (dj and dj in url)
644
+
645
+ def get_captcha_type_for_links(links):
646
+ """Determine which captcha type to use based on links"""
647
+ has_hide = any(("hide." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
648
+ has_junkies = any(is_junkies_link(l) for l in links)
649
+ has_keeplinks = any(("keeplinks." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
650
+ has_tolink = any(("tolink." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
651
+
652
+ if has_hide:
653
+ return "hide"
654
+ elif has_junkies:
655
+ return "junkies"
656
+ elif has_keeplinks:
657
+ return "keeplinks"
658
+ elif has_tolink:
659
+ return "tolink"
660
+ else:
661
+ return "cutcaptcha"
662
+
663
+ options = []
664
+ for package in protected:
665
+ pkg_id = package[0]
666
+ data = json.loads(package[1])
667
+ title = data.get("title", "Unknown")
668
+ links = data.get("links", [])
669
+ password = data.get("password", "")
670
+ mirror = data.get("mirror")
671
+
672
+ # Prioritize rapidgator links for cutcaptcha
673
+ rapid = [ln for ln in links if "rapidgator" in ln[1].lower()]
674
+ others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
675
+ prioritized = rapid + others
676
+
677
+ payload = {
678
+ "package_id": pkg_id,
679
+ "title": title,
680
+ "password": password,
681
+ "mirror": mirror,
682
+ "links": prioritized,
683
+ }
684
+ encoded = urlsafe_b64encode(json.dumps(payload).encode()).decode()
685
+ captcha_type = get_captcha_type_for_links(prioritized)
686
+
687
+ selected = "selected" if pkg_id == current_package_id else ""
688
+ # Truncate long titles for display
689
+ display_title = (title[:50] + "...") if len(title) > 53 else title
690
+ options.append(f'<option value="{captcha_type}|{quote(encoded)}" {selected}>{display_title}</option>')
691
+
692
+ options_html = "\n".join(options)
693
+
694
+ return f'''
695
+ <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;">
696
+ <label for="package-select" style="display: block; margin-bottom: 8px; font-weight: bold;">📦 Select Package:</label>
697
+ <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;">
698
+ {options_html}
699
+ </select>
700
+ </div>
701
+ <script>
702
+ document.getElementById('package-select').addEventListener('change', function() {{
703
+ const [captchaType, encodedData] = this.value.split('|');
704
+ window.location.href = '/captcha/' + captchaType + '?data=' + encodedData;
705
+ }});
706
+ </script>
707
+ '''
708
+
709
+ def render_failed_attempts_warning(package_id, include_delete_button=True, fallback_url=None):
710
+ """Render a warning block that shows after 2+ failed attempts per package_id.
711
+ Uses localStorage to track attempts by package_id to ensure reliable tracking
712
+ even when package titles are duplicated.
713
+
714
+ Attempts are NOT incremented on page load - they must be incremented by
715
+ calling window.incrementCaptchaAttempts() when user takes an action (e.g.,
716
+ clicking submit, opening bypass link).
717
+
718
+ Args:
719
+ package_id: The unique package identifier
720
+ include_delete_button: Whether to show delete button in warning
721
+ fallback_url: Optional URL to a fallback page (e.g., FileCrypt manual fallback)
722
+ """
723
+
724
+ delete_button = ""
725
+ if include_delete_button:
726
+ delete_button = render_button("Delete Package", "primary",
727
+ {"onclick": f"location.href='/captcha/delete/{package_id}'"})
728
+
729
+ fallback_link = ""
730
+ if fallback_url:
731
+ fallback_link = f'''
732
+ <p style="margin-top: 12px; margin-bottom: 8px;">
733
+ <a href="{fallback_url}" style="color: #cc0000;">Try the manual FileCrypt fallback page →</a>
734
+ </p>
735
+ '''
736
+
737
+ return f'''
738
+ <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;">
739
+ <h3 style="color: #dc2626; margin-top: 0;">⚠️ Multiple Failed Attempts Detected</h3>
740
+ <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>
741
+ <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>
742
+ {fallback_link}
743
+ <div id="warning-delete-button" style="margin-top: 12px;">
744
+ {delete_button}
745
+ </div>
746
+ </div>
747
+ <script>
748
+ (function() {{
749
+ const packageId = '{package_id}';
750
+ const storageKey = 'captcha_attempts_' + packageId;
751
+
752
+ // Get current attempt count (do NOT increment on page load)
753
+ let attempts = parseInt(localStorage.getItem(storageKey) || '0', 10);
754
+
755
+ // Show warning if 2+ failed attempts
756
+ if (attempts >= 2) {{
757
+ const warningBox = document.getElementById('failed-attempts-warning');
758
+ if (warningBox) {{
759
+ warningBox.style.display = 'block';
760
+ }}
761
+ }}
762
+
763
+ // Function to increment attempts (call this on submit/action)
764
+ window.incrementCaptchaAttempts = function() {{
765
+ let current = parseInt(localStorage.getItem(storageKey) || '0', 10);
766
+ current++;
767
+ localStorage.setItem(storageKey, current.toString());
768
+ // Show warning immediately if we hit 2+ attempts
769
+ if (current >= 2) {{
770
+ const warningBox = document.getElementById('failed-attempts-warning');
771
+ if (warningBox) {{
772
+ warningBox.style.display = 'block';
773
+ }}
774
+ }}
775
+ return current;
776
+ }};
777
+
778
+ // Function to get current attempt count
779
+ window.getCaptchaAttempts = function() {{
780
+ return parseInt(localStorage.getItem(storageKey) || '0', 10);
781
+ }};
782
+
783
+ // Function to clear attempts (call on success)
784
+ window.clearCaptchaAttempts = function() {{
785
+ localStorage.removeItem(storageKey);
786
+ }};
787
+ }})();
788
+ </script>
789
+ '''
790
+
791
+ @app.get("/captcha/filecrypt")
792
+ def serve_filecrypt_fallback():
793
+ """Dedicated FileCrypt fallback page - similar to hide/junkies/keeplinks/tolink"""
794
+ payload = decode_payload()
795
+
796
+ if "error" in payload:
797
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
798
+ <p>{payload["error"]}</p>
799
+ <p>
800
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
801
+ </p>''')
802
+
803
+ package_id = payload.get("package_id")
804
+ title = payload.get("title")
805
+ password = payload.get("password")
806
+ urls = payload.get("links")
807
+
808
+ check_package_exists(package_id)
809
+
810
+ url = urls[0][0] if isinstance(urls[0], (list, tuple)) else urls[0]
811
+
812
+ # Generate userscript URL with transfer params
813
+ base_url = request.urlparts.scheme + '://' + request.urlparts.netloc
814
+ transfer_url = f"{base_url}/captcha/quick-transfer"
815
+
816
+ url_with_quick_transfer_params = (
817
+ f"{url}?"
818
+ f"transfer_url={quote(transfer_url)}&"
819
+ f"pkg_id={quote(package_id)}&"
820
+ f"pkg_title={quote(title)}&"
821
+ f"pkg_pass={quote(password)}"
822
+ )
823
+
824
+ package_selector = render_package_selector(package_id, title)
825
+ failed_warning = render_failed_attempts_warning(package_id)
826
+
827
+ return render_centered_html(f"""
828
+ <!DOCTYPE html>
829
+ <html>
830
+ <body>
831
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
832
+ {package_selector}
833
+ {failed_warning}
834
+
835
+ <div>
836
+ <!-- Info section explaining the process -->
837
+ <div class="info-box">
838
+ <h3>ℹ️ How This Works:</h3>
839
+ <p style="margin-bottom: 8px;">
840
+ 1. Click the button below to open FileCrypt directly
841
+ </p>
842
+ <p style="margin-top: 0; margin-bottom: 8px;">
843
+ 2. Solve any CAPTCHAs on their site to reveal the download links
844
+ </p>
845
+ <p style="margin-top: 0; margin-bottom: 0;">
846
+ 3. <b>With the userscript installed</b>, links are automatically sent back to Quasarr!
847
+ </p>
848
+ </div>
849
+
850
+ <!-- One-time setup section - visually separated -->
851
+ <div id="setup-instructions" class="setup-box">
852
+ <h3>📦 First Time Setup:</h3>
853
+ <p style="margin-bottom: 8px;">
854
+ <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. Install Tampermonkey</a>
855
+ </p>
856
+ <p style="margin-top: 0; margin-bottom: 12px;">
857
+ <a href="/captcha/filecrypt.user.js" target="_blank">2. Install the FileCrypt userscript</a>
858
+ </p>
859
+ <p style="margin-top: 0;">
860
+ <button id="hide-setup-btn" type="button" class="btn-subtle">
861
+ ✅ Don't show this again
862
+ </button>
863
+ </p>
864
+ </div>
865
+
866
+ <!-- Hidden "show instructions" button -->
867
+ <div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
868
+ <button id="show-setup-btn" type="button" class="btn-subtle">
869
+ ℹ️ Show setup instructions
870
+ </button>
871
+ </div>
872
+
873
+ <!-- Primary action button -->
874
+ <p>
875
+ {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
876
+ </p>
877
+
878
+ <!-- Manual submission section -->
879
+ <div class="section-divider">
880
+ <details id="manualSubmitDetails">
881
+ <summary id="manualSubmitSummary" style="cursor: pointer;">Show Manual Submission</summary>
882
+ <div style="margin-top: 16px;">
883
+ <p style="font-size: 0.9em; margin-bottom: 16px;">
884
+ If the userscript doesn't work, you can manually paste the links or upload a DLC file:
885
+ </p>
886
+ <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data" onsubmit="if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();">
887
+ <input type="hidden" name="package_id" value="{package_id}" />
888
+ <input type="hidden" name="title" value="{title}" />
889
+ <input type="hidden" name="password" value="{password}" />
890
+
891
+ <div>
892
+ <strong>Paste the download links (one per line):</strong>
893
+ <textarea id="links-input" name="links" rows="5" style="width: 100%; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
894
+ </div>
895
+
896
+ <div>
897
+ <strong>Or upload DLC file:</strong><br>
898
+ <input type="file" id="dlc-file" name="dlc_file" accept=".dlc" />
899
+ </div>
900
+
901
+ <div>
902
+ {render_button("Submit", "primary", {"type": "submit"})}
903
+ </div>
904
+ </form>
905
+ </div>
906
+ </details>
907
+ </div>
908
+ </div>
909
+
910
+ <p>
911
+ {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
912
+ </p>
913
+ <p>
914
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
915
+ </p>
916
+
917
+ <script>
918
+ // Handle manual submission toggle text
919
+ const manualDetails = document.getElementById('manualSubmitDetails');
920
+ const manualSummary = document.getElementById('manualSubmitSummary');
921
+
922
+ if (manualDetails && manualSummary) {{
923
+ manualDetails.addEventListener('toggle', () => {{
924
+ if (manualDetails.open) {{
925
+ manualSummary.textContent = 'Hide Manual Submission';
926
+ }} else {{
927
+ manualSummary.textContent = 'Show Manual Submission';
928
+ }}
929
+ }});
930
+ }}
931
+
932
+ // Handle setup instructions hide/show
933
+ const hideSetup = localStorage.getItem('hideFileCryptFallbackSetupInstructions');
934
+ const setupBox = document.getElementById('setup-instructions');
935
+ const showLink = document.getElementById('show-instructions-link');
936
+
937
+ if (hideSetup === 'true') {{
938
+ setupBox.style.display = 'none';
939
+ showLink.style.display = 'block';
940
+ }}
941
+
942
+ // Hide setup instructions
943
+ document.getElementById('hide-setup-btn').addEventListener('click', function() {{
944
+ localStorage.setItem('hideFileCryptFallbackSetupInstructions', 'true');
945
+ setupBox.style.display = 'none';
946
+ showLink.style.display = 'block';
947
+ }});
948
+
949
+ // Show setup instructions again
950
+ document.getElementById('show-setup-btn').addEventListener('click', function() {{
951
+ localStorage.setItem('hideFileCryptFallbackSetupInstructions', 'false');
952
+ setupBox.style.display = 'block';
953
+ showLink.style.display = 'none';
954
+ }});
955
+ </script>
956
+
957
+ </body>
958
+ </html>""")
959
+
613
960
  @app.get('/captcha/quick-transfer')
614
961
  def handle_quick_transfer():
615
962
  """Handle quick transfer from userscript"""
@@ -711,7 +1058,8 @@ def setup_captcha_routes(app):
711
1058
  </p>
712
1059
  <p>
713
1060
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
714
- </p>''')
1061
+ </p>
1062
+ <script>localStorage.removeItem('captcha_attempts_{package_id}');</script>''')
715
1063
  else:
716
1064
  StatsHelper(shared_state).increment_failed_decryptions_manual()
717
1065
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
@@ -751,7 +1099,8 @@ def setup_captcha_routes(app):
751
1099
  </p>
752
1100
  <p>
753
1101
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
754
- </p>''')
1102
+ </p>
1103
+ <script>localStorage.removeItem('captcha_attempts_{package_id}');</script>''')
755
1104
  else:
756
1105
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
757
1106
  <p>Failed to delete package!</p>
@@ -826,8 +1175,56 @@ def setup_captcha_routes(app):
826
1175
  # Add bypass section
827
1176
  bypass_section = render_filecrypt_bypass_section(url, package_id, title, password)
828
1177
 
1178
+ # Add package selector and failed attempts warning
1179
+ package_selector = render_package_selector(package_id, title)
1180
+
1181
+ # Create fallback URL for the manual FileCrypt page
1182
+ fallback_payload = {
1183
+ "package_id": package_id,
1184
+ "title": title,
1185
+ "password": password,
1186
+ "mirror": desired_mirror,
1187
+ "links": prioritized_links,
1188
+ }
1189
+ fallback_encoded = urlsafe_b64encode(json.dumps(fallback_payload).encode()).decode()
1190
+ filecrypt_fallback_url = f"/captcha/filecrypt?data={quote(fallback_encoded)}"
1191
+
1192
+ failed_warning = render_failed_attempts_warning(package_id, include_delete_button=False,
1193
+ fallback_url=filecrypt_fallback_url) # Delete button is already below
1194
+
1195
+ # Escape title for safe use in JavaScript string
1196
+ escaped_title_js = title.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r')
1197
+
829
1198
  content = render_centered_html(r'''
1199
+ <style>
1200
+ @media (max-width: 600px) {
1201
+ .package-selector,
1202
+ #failed-attempts-warning {
1203
+ margin-left: 0 !important;
1204
+ margin-right: 0 !important;
1205
+ padding-left: 8px !important;
1206
+ padding-right: 8px !important;
1207
+ border-radius: 0 !important;
1208
+ border-left: none !important;
1209
+ border-right: none !important;
1210
+ }
1211
+ }
1212
+ </style>
830
1213
  <script type="text/javascript">
1214
+ // Package title for result display
1215
+ var packageTitleText = "''' + escaped_title_js + r'''";
1216
+
1217
+ // Check if we should redirect to fallback due to failed attempts
1218
+ (function() {
1219
+ const storageKey = 'captcha_attempts_''' + package_id + r'''';
1220
+ const attempts = parseInt(localStorage.getItem(storageKey) || '0', 10);
1221
+ if (attempts >= 2) {
1222
+ // Redirect to FileCrypt fallback page
1223
+ window.location.href = '''' + filecrypt_fallback_url + r'''';
1224
+ return;
1225
+ }
1226
+ })();
1227
+
831
1228
  var api_key = "''' + obfuscated.captcha_values()["api_key"] + r'''";
832
1229
  var endpoint = '/' + window.location.pathname.split('/')[1] + '/' + api_key + '.html';
833
1230
  var solveAnotherHtml = `<p>''' + solve_another_html + r'''</p><p>''' + back_button_html + r'''</p>`;
@@ -839,12 +1236,14 @@ def setup_captcha_routes(app):
839
1236
  document.getElementById("delete-package-section").style.display = "none";
840
1237
  document.getElementById("back-button-section").style.display = "none";
841
1238
  document.getElementById("bypass-section").style.display = "none";
842
-
843
- // Remove width limit on result screen
844
- var packageTitle = document.getElementById("package-title");
845
- packageTitle.style.maxWidth = "none";
846
-
847
- document.getElementById("captcha-key").innerText = 'Using result "' + token + '" to decrypt links...';
1239
+ // Hide package selector and warning on token submission
1240
+ var pkgSelector = document.getElementById("package-selector-section");
1241
+ if (pkgSelector) pkgSelector.style.display = "none";
1242
+ var warnBox = document.getElementById("failed-attempts-warning");
1243
+ if (warnBox) warnBox.style.display = "none";
1244
+
1245
+ // Add package title to result area
1246
+ document.getElementById("captcha-key").innerHTML = '<p style="word-break: break-all;"><b>Package:</b> ' + packageTitleText + '</p><p style="word-break: break-all;">Using result "' + token + '" to decrypt links...</p>';
848
1247
  var link = document.getElementById("link-hidden").value;
849
1248
  const fullPath = '/captcha/decrypt-filecrypt';
850
1249
 
@@ -867,9 +1266,17 @@ def setup_captcha_routes(app):
867
1266
  if (data.success) {
868
1267
  document.getElementById("captcha-key").insertAdjacentHTML('afterend',
869
1268
  '<p>✅ Successful!</p>');
1269
+ // Clear failed attempts on success
1270
+ if (typeof clearCaptchaAttempts === 'function') {
1271
+ clearCaptchaAttempts();
1272
+ }
870
1273
  } else {
871
1274
  document.getElementById("captcha-key").insertAdjacentHTML('afterend',
872
1275
  '<p>Failed. Check console for details!</p>');
1276
+ // Increment failed attempts on failure
1277
+ if (typeof incrementCaptchaAttempts === 'function') {
1278
+ incrementCaptchaAttempts();
1279
+ }
873
1280
  }
874
1281
 
875
1282
  // Show appropriate button based on whether more CAPTCHAs exist
@@ -885,7 +1292,10 @@ def setup_captcha_routes(app):
885
1292
  ''' + obfuscated.cutcaptcha_custom_js() + f'''</script>
886
1293
  <div>
887
1294
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
888
- <p id="package-title" style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
1295
+ <div id="package-selector-section">
1296
+ {package_selector}
1297
+ </div>
1298
+ {failed_warning}
889
1299
  <div id="captcha-key"></div>
890
1300
  {link_select}<br><br>
891
1301
  <input type="hidden" id="link-hidden" value="{prioritized_links[0][0]}" />
@@ -1068,7 +1478,8 @@ def setup_captcha_routes(app):
1068
1478
  </p>
1069
1479
  <p>
1070
1480
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
1071
- </p>''')
1481
+ </p>
1482
+ <script>localStorage.removeItem('captcha_attempts_{package_id}');</script>''')
1072
1483
  else:
1073
1484
  StatsHelper(shared_state).increment_failed_decryptions_manual()
1074
1485
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
@@ -1113,38 +1524,17 @@ def setup_captcha_routes(app):
1113
1524
  info(f"Decrypting links for {title}")
1114
1525
  decrypted = get_filecrypt_links(shared_state, token, title, link, password=password, mirror=mirror)
1115
1526
  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
-
1527
+ links = decrypted.get("links", [])
1528
+ info(f"Decrypted {len(links)} download links for {title}")
1529
+ if not links:
1530
+ raise ValueError("No download links found after decryption")
1531
+ downloaded = shared_state.download_package(links, title, password, package_id)
1532
+ if downloaded:
1533
+ StatsHelper(shared_state).increment_package_with_links(links)
1534
+ shared_state.get_db("protected").delete(package_id)
1136
1535
  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")
1536
+ links = []
1537
+ raise RuntimeError("Submitting Download to JDownloader failed")
1148
1538
  else:
1149
1539
  raise ValueError("No download links found")
1150
1540
 
@@ -1162,171 +1552,3 @@ def setup_captcha_routes(app):
1162
1552
  has_more_captchas = bool(remaining_protected)
1163
1553
 
1164
1554
  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>""")