quasarr 2.4.8__py3-none-any.whl → 2.4.10__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (76) hide show
  1. quasarr/__init__.py +134 -70
  2. quasarr/api/__init__.py +40 -31
  3. quasarr/api/arr/__init__.py +116 -108
  4. quasarr/api/captcha/__init__.py +262 -137
  5. quasarr/api/config/__init__.py +76 -46
  6. quasarr/api/packages/__init__.py +138 -102
  7. quasarr/api/sponsors_helper/__init__.py +29 -16
  8. quasarr/api/statistics/__init__.py +19 -19
  9. quasarr/downloads/__init__.py +165 -72
  10. quasarr/downloads/linkcrypters/al.py +35 -18
  11. quasarr/downloads/linkcrypters/filecrypt.py +107 -52
  12. quasarr/downloads/linkcrypters/hide.py +5 -6
  13. quasarr/downloads/packages/__init__.py +342 -177
  14. quasarr/downloads/sources/al.py +191 -100
  15. quasarr/downloads/sources/by.py +31 -13
  16. quasarr/downloads/sources/dd.py +27 -14
  17. quasarr/downloads/sources/dj.py +1 -3
  18. quasarr/downloads/sources/dl.py +126 -71
  19. quasarr/downloads/sources/dt.py +11 -5
  20. quasarr/downloads/sources/dw.py +28 -14
  21. quasarr/downloads/sources/he.py +32 -24
  22. quasarr/downloads/sources/mb.py +19 -9
  23. quasarr/downloads/sources/nk.py +14 -10
  24. quasarr/downloads/sources/nx.py +8 -18
  25. quasarr/downloads/sources/sf.py +45 -20
  26. quasarr/downloads/sources/sj.py +1 -3
  27. quasarr/downloads/sources/sl.py +9 -5
  28. quasarr/downloads/sources/wd.py +32 -12
  29. quasarr/downloads/sources/wx.py +35 -21
  30. quasarr/providers/auth.py +42 -37
  31. quasarr/providers/cloudflare.py +28 -30
  32. quasarr/providers/hostname_issues.py +2 -1
  33. quasarr/providers/html_images.py +2 -2
  34. quasarr/providers/html_templates.py +22 -14
  35. quasarr/providers/imdb_metadata.py +149 -80
  36. quasarr/providers/jd_cache.py +131 -39
  37. quasarr/providers/log.py +1 -1
  38. quasarr/providers/myjd_api.py +260 -196
  39. quasarr/providers/notifications.py +53 -41
  40. quasarr/providers/obfuscated.py +9 -4
  41. quasarr/providers/sessions/al.py +71 -55
  42. quasarr/providers/sessions/dd.py +21 -14
  43. quasarr/providers/sessions/dl.py +30 -19
  44. quasarr/providers/sessions/nx.py +23 -14
  45. quasarr/providers/shared_state.py +292 -141
  46. quasarr/providers/statistics.py +75 -43
  47. quasarr/providers/utils.py +33 -27
  48. quasarr/providers/version.py +45 -14
  49. quasarr/providers/web_server.py +10 -5
  50. quasarr/search/__init__.py +30 -18
  51. quasarr/search/sources/al.py +124 -73
  52. quasarr/search/sources/by.py +110 -59
  53. quasarr/search/sources/dd.py +57 -35
  54. quasarr/search/sources/dj.py +69 -48
  55. quasarr/search/sources/dl.py +159 -100
  56. quasarr/search/sources/dt.py +110 -74
  57. quasarr/search/sources/dw.py +121 -61
  58. quasarr/search/sources/fx.py +108 -62
  59. quasarr/search/sources/he.py +78 -49
  60. quasarr/search/sources/mb.py +96 -48
  61. quasarr/search/sources/nk.py +80 -50
  62. quasarr/search/sources/nx.py +91 -62
  63. quasarr/search/sources/sf.py +171 -106
  64. quasarr/search/sources/sj.py +69 -48
  65. quasarr/search/sources/sl.py +115 -71
  66. quasarr/search/sources/wd.py +67 -44
  67. quasarr/search/sources/wx.py +188 -123
  68. quasarr/storage/config.py +65 -52
  69. quasarr/storage/setup.py +238 -140
  70. quasarr/storage/sqlite_database.py +10 -4
  71. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/METADATA +4 -3
  72. quasarr-2.4.10.dist-info/RECORD +81 -0
  73. quasarr-2.4.8.dist-info/RECORD +0 -81
  74. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/WHEEL +0 -0
  75. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/entry_points.txt +0 -0
  76. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/licenses/LICENSE +0 -0
@@ -4,24 +4,23 @@
4
4
 
5
5
  import json
6
6
  import re
7
- from base64 import urlsafe_b64encode, urlsafe_b64decode
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 request, response, redirect, HTTPResponse
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 get_filecrypt_links, DLC
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 info, debug
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('\\', '\\\\').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('/captcha')
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('package_id')
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 = link[1] if isinstance(link, (list, tuple)) and len(link) > 1 else ""
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
- ("hide." in link[0] if isinstance(link, (list, tuple)) else "hide." in link)
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
- ("keeplinks." in link[0] if isinstance(link, (list, tuple)) else "keeplinks." in link)
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
- ("tolink." in link[0] if isinstance(link, (list, tuple)) else "tolink." in link)
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('data')
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(url, package_id, title, password, provider_type="junkies"):
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 = {"hide": "Hide", "junkies": "Junkies", "keeplinks": "KeepLinks", "tolink": "ToLink"}
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 + '://' + request.urlparts.netloc
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'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
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'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
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'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
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'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
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('/captcha/filecrypt.user.js')
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 = 'application/javascript'
511
+ response.content_type = "application/javascript"
492
512
  return content
493
513
 
494
- @app.get('/captcha/hide.user.js')
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 = 'application/javascript'
517
+ response.content_type = "application/javascript"
498
518
  return content
499
519
 
500
- @app.get('/captcha/junkies.user.js')
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 = 'application/javascript'
526
+ response.content_type = "application/javascript"
507
527
  return content
508
528
 
509
- @app.get('/captcha/keeplinks.user.js')
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 = 'application/javascript'
532
+ response.content_type = "application/javascript"
513
533
  return content
514
534
 
515
- @app.get('/captcha/tolink.user.js')
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 = 'application/javascript'
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 + '://' + request.urlparts.netloc
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 = link[1] if isinstance(link, (list, tuple)) and len(link) > 1 else ""
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(("hide." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
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(("keeplinks." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
673
- has_tolink = any(("tolink." in (l[0] if isinstance(l, (list, tuple)) else l)) for l in links)
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(f'<option value="{captcha_type}|{quote(encoded)}" {selected}>{display_title}</option>')
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(package_id, include_delete_button=True, fallback_url=None):
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("Delete Package", "primary",
752
- {"onclick": f"location.href='/captcha/delete/{package_id}'"})
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 + '://' + request.urlparts.netloc
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'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
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('/captcha/quick-transfer')
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('pkg_id')
987
- compressed_links = request.query.get('links', '')
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 += '=' * padding
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(decoded, -15) # -15 = raw deflate, no zlib header
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('utf-8')
1068
+ links_text = decompressed.decode("utf-8")
1029
1069
 
1030
1070
  # Parse links and restore protocols
1031
- raw_links = [link.strip() for link in links_text.split('\n') if link.strip()]
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(('http://', 'https://')):
1035
- link = 'https://' + 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(links, title, password, package_id)
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("protected").retrieve_all_titles()
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("Solve another CAPTCHA", "primary",
1069
- {"onclick": "location.href='/captcha'"})
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('/captcha/delete/<package_id>')
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("Solve another CAPTCHA", "primary", {
1109
- "onclick": "location.href='/captcha'",
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('/captcha/cutcaptcha')
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'''<div id="mirrors-select">
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("Solve another CAPTCHA", "primary", {"onclick": "location.href='/captcha'"})
1192
- back_button_html = render_button("Back", "secondary", {"onclick": "location.href='/'"})
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(url, package_id, title, password)
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(json.dumps(fallback_payload).encode()).decode()
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(package_id, include_delete_button=False,
1215
- fallback_url=filecrypt_fallback_url) # Delete button is already below
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 = title.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r')
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'<p>{render_button("Source", "secondary", {"onclick": f"window.open(\'{js_single_quoted_string_safe(original_url)}\', \'_blank\')"})}</p>'
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(r'''
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 = "''' + escaped_title_js + r'''";
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_''' + package_id + r'''';
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 = '''' + filecrypt_fallback_url + r'''';
1320
+ window.location.href = """
1321
+ " + filecrypt_fallback_url + r"
1322
+ ''';
1249
1323
  return;
1250
1324
  }
1251
1325
  })();
1252
1326
 
1253
- var api_key = "''' + obfuscated.captcha_values()["api_key"] + r'''";
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>''' + solve_another_html + r'''</p><p>''' + back_button_html + r'''</p>`;
1256
- var noMoreHtml = `<p><b>No more CAPTCHAs</b></p><p>''' + back_button_html + r'''</p>`;
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
- ''' + f'''package_id: '{package_id}',
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
- ''' + obfuscated.cutcaptcha_custom_js() + f'''</script>
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
- ''' + source_button_html + f'''
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('/captcha/<captcha_id>.html')
1443
+ @app.post("/captcha/<captcha_id>.html")
1355
1444
  def proxy_html(captcha_id):
1356
- target_url = f"{obfuscated.captcha_values()["url"]}/captcha/{captcha_id}.html"
1445
+ target_url = f"{obfuscated.captcha_values()['url']}/captcha/{captcha_id}.html"
1357
1446
 
1358
- headers = {key: value for key, value in request.headers.items() if key != 'Host'}
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('Content-Type')
1453
+ response.content_type = resp.headers.get("Content-Type")
1363
1454
 
1364
1455
  content = resp.text
1365
1456
  content = re.sub(
1366
- r'''<script\s+src="/(jquery(?:-ui|\.ui\.touch-punch\.min)?\.js)(?:\?[^"]*)?"></script>''',
1367
- r'''<script src="/captcha/js/\1"></script>''',
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 = 'text/html'
1462
+ response.content_type = "text/html"
1372
1463
  return content
1373
1464
 
1374
- @app.post('/captcha/<captcha_id>.json')
1465
+ @app.post("/captcha/<captcha_id>.json")
1375
1466
  def proxy_json(captcha_id):
1376
- target_url = f"{obfuscated.captcha_values()["url"]}/captcha/{captcha_id}.json"
1467
+ target_url = f"{obfuscated.captcha_values()['url']}/captcha/{captcha_id}.json"
1377
1468
 
1378
- headers = {key: value for key, value in request.headers.items() if key != 'Host'}
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('Content-Type')
1475
+ response.content_type = resp.headers.get("Content-Type")
1383
1476
  return resp.content
1384
1477
 
1385
- @app.get('/captcha/js/<filename>')
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 = 'application/javascript'
1488
+ response.content_type = "application/javascript"
1396
1489
  return upstream_resp.iter_content(chunk_size=8192)
1397
1490
 
1398
- @app.get('/captcha/<captcha_id>/<uuid>/<filename>')
1491
+ @app.get("/captcha/<captcha_id>/<uuid>/<filename>")
1399
1492
  def proxy_pngs(captcha_id, uuid, filename):
1400
- new_url = f"{obfuscated.captcha_values()["url"]}/captcha/{captcha_id}/{uuid}/{filename}"
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 = 'image/png'
1406
- response.headers['Content-Disposition'] = f'inline; filename="{filename}"'
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('/captcha/<captcha_id>/check')
1506
+ @app.post("/captcha/<captcha_id>/check")
1414
1507
  def proxy_check(captcha_id):
1415
- new_url = f"{obfuscated.captcha_values()["url"]}/captcha/{captcha_id}/check"
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 ['content-encoding', 'transfer-encoding', 'content-length', 'connection']:
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('/captcha/bypass-submit')
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('package_id')
1432
- title = request.forms.get('title')
1433
- password = request.forms.get('password', '')
1434
- links_input = request.forms.get('links', '').strip()
1435
- dlc_upload = request.files.get('dlc_file')
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 = [link.strip() for link in links_input.split('\n') if link.strip()]
1450
- links = [l for l in raw_links if l.lower().startswith(("http://", "https://"))]
1451
-
1452
- info(f"Received {len(links)} valid direct download links "
1453
- f"(from {len(raw_links)} provided)")
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(links, title, password, package_id)
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("protected").retrieve_all_titles()
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("Solve another CAPTCHA", "primary", {
1494
- "onclick": "location.href='/captcha'",
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('/captcha/decrypt-filecrypt')
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 {"success": False, "title": "No protected packages found! CAPTCHA not needed."}
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('token')
1542
- package_id = data.get('package_id')
1543
- title = data.get('title')
1544
- link = data.get('link')
1545
- password = data.get('password')
1546
- mirror = None if (mirror := data.get('mirror')) == "None" else mirror
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(shared_state, token, title, link, password=password, mirror=mirror)
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(links, title, password, package_id)
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 {"success": success, "title": title, "has_more_captchas": has_more_captchas}
1701
+ return {
1702
+ "success": success,
1703
+ "title": title,
1704
+ "has_more_captchas": has_more_captchas,
1705
+ }