quasarr 1.16.10__py3-none-any.whl → 1.17.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.

@@ -11,7 +11,7 @@ import requests
11
11
  from bottle import request, response, redirect
12
12
 
13
13
  import quasarr.providers.html_images as images
14
- from quasarr.downloads.linkcrypters.filecrypt import get_filecrypt_links
14
+ from quasarr.downloads.linkcrypters.filecrypt import get_filecrypt_links, DLC
15
15
  from quasarr.downloads.packages import delete_package
16
16
  from quasarr.providers import shared_state
17
17
  from quasarr.providers.html_templates import render_button, render_centered_html
@@ -65,13 +65,17 @@ def setup_captcha_routes(app):
65
65
  others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
66
66
  prioritized_links = rapid + others
67
67
 
68
+ # This is required for bypass on circlecaptcha
69
+ original_url = data.get("original_url", "")
70
+
68
71
  payload = {
69
72
  "package_id": package_id,
70
73
  "title": title,
71
74
  "password": password,
72
75
  "mirror": desired_mirror,
73
76
  "session": session,
74
- "links": prioritized_links
77
+ "links": prioritized_links,
78
+ "original_url": original_url
75
79
  }
76
80
 
77
81
  encoded_payload = urlsafe_b64encode(json.dumps(payload).encode()).decode()
@@ -97,6 +101,34 @@ def setup_captcha_routes(app):
97
101
  except Exception as e:
98
102
  return {"error": f"Failed to decode payload: {str(e)}"}
99
103
 
104
+ def render_bypass_section(url, package_id, title, password):
105
+ """Render the bypass UI section for both cutcaptcha and circle captcha pages"""
106
+ return f'''
107
+ <div style="margin-top: 40px; padding-top: 20px; border-top: 2px solid #ccc;">
108
+ <h3>Bypass CAPTCHA</h3>
109
+ <a href="{url}" target="_blank">Protected Link</a>
110
+ <form id="bypass-form" action="/captcha/bypass-submit" method="post" enctype="multipart/form-data">
111
+ <input type="hidden" name="package_id" value="{package_id}" />
112
+ <input type="hidden" name="title" value="{title}" />
113
+ <input type="hidden" name="password" value="{password}" />
114
+
115
+ <div style="margin-bottom: 15px;">
116
+ <label for="links-input"><b>Paste direct download links (one per line):</b></label><br>
117
+ <textarea id="links-input" name="links" rows="5" style="width: 100%; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
118
+ </div>
119
+
120
+ <div style="margin-bottom: 15px;">
121
+ <label for="dlc-file"><b>Or upload DLC file:</b></label><br>
122
+ <input type="file" id="dlc-file" name="dlc_file" accept=".dlc" />
123
+ </div>
124
+
125
+ <div>
126
+ {render_button("Submit Bypass", "primary", {"type": "submit"})}
127
+ </div>
128
+ </form>
129
+ </div>
130
+ '''
131
+
100
132
  @app.get('/captcha/delete/<package_id>')
101
133
  def delete_captcha_package(package_id):
102
134
  success = delete_package(shared_state, package_id)
@@ -188,6 +220,11 @@ def setup_captcha_routes(app):
188
220
  solve_another_html = render_button("Solve another CAPTCHA", "primary", {"onclick": "location.href='/captcha'"})
189
221
  back_button_html = render_button("Back", "secondary", {"onclick": "location.href='/'"})
190
222
 
223
+ url = prioritized_links[0][0]
224
+
225
+ # Add bypass section
226
+ bypass_section = render_bypass_section(url, package_id, title, password)
227
+
191
228
  content = render_centered_html(r'''
192
229
  <script type="text/javascript">
193
230
  var api_key = "''' + captcha_values()["api_key"] + r'''";
@@ -200,6 +237,7 @@ def setup_captcha_routes(app):
200
237
  document.getElementById("mirrors-select").remove();
201
238
  document.getElementById("delete-package-section").style.display = "none";
202
239
  document.getElementById("back-button-section").style.display = "none";
240
+ document.getElementById("bypass-section").style.display = "none";
203
241
 
204
242
  // Remove width limit on result screen
205
243
  var packageTitle = document.getElementById("package-title");
@@ -247,6 +285,7 @@ def setup_captcha_routes(app):
247
285
  <div>
248
286
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
249
287
  <p id="package-title" style="max-width: 370px; word-wrap: break-word; overflow-wrap: break-word;"><b>Package:</b> {title}</p>
288
+ <h3>Solve CAPTCHA</h3>
250
289
  <div id="captcha-key"></div>
251
290
  {link_select}<br><br>
252
291
  <input type="hidden" id="link-hidden" value="{prioritized_links[0][0]}" />
@@ -267,6 +306,9 @@ def setup_captcha_routes(app):
267
306
  <p>
268
307
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
269
308
  </p>
309
+ </div>
310
+ <div id="bypass-section">
311
+ {bypass_section}
270
312
  </div>
271
313
  </div>
272
314
  </html>''')
@@ -346,6 +388,104 @@ def setup_captcha_routes(app):
346
388
  response.set_header(header, resp.headers[header])
347
389
  return resp.content
348
390
 
391
+ @app.post('/captcha/bypass-submit')
392
+ def handle_bypass_submit():
393
+ """Handle bypass submission with either links or DLC file"""
394
+ try:
395
+ package_id = request.forms.get('package_id')
396
+ title = request.forms.get('title')
397
+ password = request.forms.get('password', '')
398
+ links_input = request.forms.get('links', '').strip()
399
+ dlc_upload = request.files.get('dlc_file')
400
+
401
+ if not package_id or not title:
402
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
403
+ <p><b>Error:</b> Missing package information.</p>
404
+ <p>
405
+ {render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
406
+ </p>''')
407
+
408
+ # Process links input
409
+ if links_input:
410
+ info(f"Processing direct links bypass for {title}")
411
+ links = [link.strip() for link in links_input.split('\n') if link.strip()]
412
+ info(f"Received {len(links)} direct download links")
413
+
414
+ # Process DLC file
415
+ elif dlc_upload:
416
+ info(f"Processing DLC file bypass for {title}")
417
+ dlc_content = dlc_upload.file.read()
418
+ try:
419
+ decrypted_links = DLC(shared_state, dlc_content).decrypt()
420
+ if decrypted_links:
421
+ links = decrypted_links
422
+ info(f"Decrypted {len(links)} links from DLC file")
423
+ else:
424
+ raise ValueError("DLC decryption returned no links")
425
+ except Exception as e:
426
+ info(f"DLC decryption failed: {e}")
427
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
428
+ <p><b>Error:</b> Failed to decrypt DLC file: {str(e)}</p>
429
+ <p>
430
+ {render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
431
+ </p>''')
432
+ else:
433
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
434
+ <p><b>Error:</b> Please provide either links or a DLC file.</p>
435
+ <p>
436
+ {render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
437
+ </p>''')
438
+
439
+ # Download the package
440
+ if links:
441
+ downloaded = shared_state.download_package(links, title, password, package_id)
442
+ if downloaded:
443
+ StatsHelper(shared_state).increment_package_with_links(links)
444
+ StatsHelper(shared_state).increment_captcha_decryptions_manual()
445
+ shared_state.get_db("protected").delete(package_id)
446
+
447
+ # Check if there are more CAPTCHAs to solve
448
+ remaining_protected = shared_state.get_db("protected").retrieve_all_titles()
449
+ has_more_captchas = bool(remaining_protected)
450
+
451
+ if has_more_captchas:
452
+ solve_button = render_button("Solve another CAPTCHA", "primary", {
453
+ "onclick": "location.href='/captcha'",
454
+ })
455
+ else:
456
+ solve_button = "<b>No more CAPTCHAs</b>"
457
+
458
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
459
+ <p><b>Success!</b> Package "{title}" bypassed and submitted to JDownloader.</p>
460
+ <p>{len(links)} link(s) processed.</p>
461
+ <p>
462
+ {solve_button}
463
+ </p>
464
+ <p>
465
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
466
+ </p>''')
467
+ else:
468
+ StatsHelper(shared_state).increment_failed_decryptions_manual()
469
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
470
+ <p><b>Error:</b> Failed to submit package to JDownloader.</p>
471
+ <p>
472
+ {render_button("Try Again", "secondary", {"onclick": "location.href='/captcha'"})}
473
+ </p>''')
474
+ else:
475
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
476
+ <p><b>Error:</b> No valid links found.</p>
477
+ <p>
478
+ {render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
479
+ </p>''')
480
+
481
+ except Exception as e:
482
+ info(f"Bypass submission error: {e}")
483
+ return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
484
+ <p><b>Error:</b> {str(e)}</p>
485
+ <p>
486
+ {render_button("Back", "secondary", {"onclick": "location.href='/captcha'"})}
487
+ </p>''')
488
+
349
489
  @app.post('/captcha/decrypt-filecrypt')
350
490
  def submit_token():
351
491
  protected = shared_state.get_db("protected").retrieve_all_titles()
@@ -382,7 +522,8 @@ def setup_captcha_routes(app):
382
522
  "size_mb": 0,
383
523
  "password": password,
384
524
  "mirror": mirror,
385
- "session": session
525
+ "session": session,
526
+ "original_url": link
386
527
  })
387
528
  shared_state.get_db("protected").update_store(package_id, blob)
388
529
  info(f"Another CAPTCHA solution is required for {mirror} link: {replace_url}")
@@ -432,20 +573,26 @@ def setup_captcha_routes(app):
432
573
  package_id = payload.get("package_id")
433
574
  session_id = payload.get("session")
434
575
  title = payload.get("title", "Unknown Package")
576
+ password = payload.get("password", "")
577
+ original_url = payload.get("original_url", "")
435
578
  url = payload.get("links")[0] if payload.get("links") else None
436
579
 
437
580
  if not url or not session_id or not package_id:
438
581
  response.status = 400
439
582
  return "Missing required parameters"
440
583
 
584
+ # Add bypass section
585
+ bypass_section = render_bypass_section(original_url, package_id, title, password)
586
+
441
587
  return render_centered_html(f"""
442
588
  <!DOCTYPE html>
443
589
  <html>
444
590
  <body>
445
591
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
446
592
  <p><b>Package:</b> {title}</p>
447
- <form action="/captcha/decrypt-filecrypt-circle?url={url}&session_id={session_id}&&package_id={package_id}" method="post">
448
- <input type="image" src="/captcha/circle.php?url={url}&session_id={session_id}" name="button" alt="Captcha">
593
+ <h3>Solve CAPTCHA</h3>
594
+ <form action="/captcha/decrypt-filecrypt-circle?url={url}&session_id={session_id}&package_id={package_id}" method="post">
595
+ <input type="image" src="/captcha/circle.php?url={url}&session_id={session_id}" name="button" alt="Circle CAPTCHA">
449
596
  </form>
450
597
  <p>
451
598
  {render_button("Delete Package", "secondary", {"onclick": f"location.href='/captcha/delete/{package_id}'"})}
@@ -453,6 +600,7 @@ def setup_captcha_routes(app):
453
600
  <p>
454
601
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
455
602
  </p>
603
+ {bypass_section}
456
604
  </body>
457
605
  </html>""")
458
606
 
@@ -20,27 +20,34 @@ from quasarr.providers.log import info, debug
20
20
 
21
21
  class CNL:
22
22
  def __init__(self, crypted_data):
23
+ debug("Initializing CNL with crypted_data.")
23
24
  self.crypted_data = crypted_data
24
25
 
25
26
  def jk_eval(self, f_def):
27
+ debug("Evaluating JavaScript key function.")
26
28
  js_code = f"""
27
29
  {f_def}
28
30
  f();
29
31
  """
30
32
 
31
33
  result = dukpy.evaljs(js_code).strip()
32
-
34
+ debug("JavaScript evaluation complete.")
33
35
  return result
34
36
 
35
37
  def aes_decrypt(self, data, key):
38
+ debug("Starting AES decrypt.")
36
39
  try:
37
40
  encrypted_data = base64.b64decode(data)
41
+ debug("Base64 decode for AES decrypt successful.")
38
42
  except Exception as e:
43
+ debug("Base64 decode for AES decrypt failed.")
39
44
  raise ValueError("Failed to decode base64 data") from e
40
45
 
41
46
  try:
42
47
  key_bytes = bytes.fromhex(key)
48
+ debug("Key successfully converted from hex.")
43
49
  except Exception as e:
50
+ debug("Failed converting key from hex.")
44
51
  raise ValueError("Failed to convert key to bytes") from e
45
52
 
46
53
  iv = key_bytes
@@ -48,26 +55,33 @@ class CNL:
48
55
 
49
56
  try:
50
57
  decrypted_data = cipher.decrypt(encrypted_data)
58
+ debug("AES decrypt operation successful.")
51
59
  except ValueError as e:
60
+ debug("AES decrypt operation failed.")
52
61
  raise ValueError("Decryption failed") from e
53
62
 
54
63
  try:
55
- return decrypted_data.decode('utf-8').replace('\x00', '').replace('\x08', '')
64
+ decoded = decrypted_data.decode('utf-8').replace('\x00', '').replace('\x08', '')
65
+ debug("Decoded AES output successfully.")
66
+ return decoded
56
67
  except UnicodeDecodeError as e:
68
+ debug("Failed decoding decrypted AES output.")
57
69
  raise ValueError("Failed to decode decrypted data") from e
58
70
 
59
71
  def decrypt(self):
72
+ debug("Starting Click'N'Load decrypt sequence.")
60
73
  crypted = self.crypted_data[2]
61
74
  jk = "function f(){ return \'" + self.crypted_data[1] + "';}"
62
75
  key = self.jk_eval(jk)
63
76
  uncrypted = self.aes_decrypt(crypted, key)
64
77
  urls = [result for result in uncrypted.split("\r\n") if len(result) > 0]
65
-
78
+ debug(f"Extracted {len(urls)} URLs from CNL decrypt.")
66
79
  return urls
67
80
 
68
81
 
69
82
  class DLC:
70
83
  def __init__(self, shared_state, dlc_file):
84
+ debug("Initializing DLC decrypt handler.")
71
85
  self.shared_state = shared_state
72
86
  self.data = dlc_file
73
87
  self.KEY = b"cb99b5cbc24db398"
@@ -75,6 +89,7 @@ class DLC:
75
89
  self.API_URL = "http://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=pylo&data="
76
90
 
77
91
  def parse_packages(self, start_node):
92
+ debug("Parsing DLC packages from XML.")
78
93
  return [
79
94
  (
80
95
  base64.b64decode(node.getAttribute("name")).decode("utf-8"),
@@ -84,41 +99,51 @@ class DLC:
84
99
  ]
85
100
 
86
101
  def parse_links(self, start_node):
102
+ debug("Parsing DLC links in package.")
87
103
  return [
88
104
  base64.b64decode(node.getElementsByTagName("url")[0].firstChild.data).decode("utf-8")
89
105
  for node in start_node.getElementsByTagName("file")
90
106
  ]
91
107
 
92
108
  def decrypt(self):
109
+ debug("Starting DLC decrypt flow.")
93
110
  if not isinstance(self.data, bytes):
111
+ debug("DLC data type invalid.")
94
112
  raise TypeError("data must be bytes.")
95
113
 
96
114
  all_urls = []
97
115
 
98
116
  try:
117
+ debug("Preparing DLC data buffer.")
99
118
  data = self.data.strip()
100
-
101
119
  data += b"=" * (-len(data) % 4)
102
120
 
103
121
  dlc_key = data[-88:].decode("utf-8")
104
122
  dlc_data = base64.b64decode(data[:-88])
123
+ debug("DLC base64 decode successful.")
105
124
 
106
125
  headers = {'User-Agent': self.shared_state.values["user_agent"]}
107
126
 
127
+ debug("Requesting DLC decryption service.")
108
128
  dlc_content = requests.get(self.API_URL + dlc_key, headers=headers, timeout=10).content.decode("utf-8")
109
129
 
110
130
  rc = base64.b64decode(re.search(r"<rc>(.+)</rc>", dlc_content, re.S).group(1))[:16]
131
+ debug("Received DLC RC block.")
111
132
 
112
133
  cipher = AES.new(self.KEY, AES.MODE_CBC, self.IV)
113
134
  key = iv = cipher.decrypt(rc)
135
+ debug("Decrypted DLC key material.")
114
136
 
115
137
  cipher = AES.new(key, AES.MODE_CBC, iv)
116
138
  xml_data = base64.b64decode(cipher.decrypt(dlc_data)).decode("utf-8")
139
+ debug("Final DLC decrypt successful.")
117
140
 
118
141
  root = xml.dom.minidom.parseString(xml_data).documentElement
119
142
  content_node = root.getElementsByTagName("content")[0]
143
+ debug("Parsed DLC XML content.")
120
144
 
121
145
  packages = self.parse_packages(content_node)
146
+ debug(f"Found {len(packages)} DLC packages.")
122
147
 
123
148
  for package in packages:
124
149
  urls = package[1]
@@ -128,80 +153,83 @@ class DLC:
128
153
  info("DLC Error: " + str(e))
129
154
  return None
130
155
 
156
+ debug(f"DLC decrypt yielded {len(all_urls)} URLs.")
131
157
  return all_urls
132
158
 
133
159
 
134
160
  def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=None):
135
- """
136
- Robust Filecrypt fetch:
137
- - Always check & bypass Cloudflare with FlareSolverr when necessary.
138
- - Detect password input more reliably.
139
- - Use session consistently and update user-agent cleanly.
140
- """
141
-
142
161
  info("Attempting to decrypt Filecrypt link: " + url)
162
+ debug("Initializing Filecrypt session & headers.")
143
163
  session = requests.Session()
144
164
  headers = {'User-Agent': shared_state.values["user_agent"]}
145
165
 
146
- # Ensure we are not blocked by Cloudflare before parsing or posting
166
+ debug("Ensuring Cloudflare bypass is ready.")
147
167
  session, headers, output = ensure_session_cf_bypassed(info, shared_state, session, url, headers)
148
168
  if not session or not output:
169
+ debug("Cloudflare bypass failed.")
149
170
  return False
150
171
 
151
172
  soup = BeautifulSoup(output.text, 'html.parser')
173
+ debug("Parsed initial Filecrypt HTML.")
152
174
 
153
175
  password_field = None
154
176
  try:
155
- # Search for input elements that look like password fields:
177
+ debug("Attempting password field auto-detection.")
156
178
  input_elem = soup.find('input', attrs={'type': 'password'})
157
179
  if not input_elem:
158
180
  input_elem = soup.find('input', placeholder=lambda v: v and 'password' in v.lower())
159
181
  if not input_elem:
160
- # fallback: name contains 'pass' or 'password'
161
182
  input_elem = soup.find('input',
162
183
  attrs={'name': lambda v: v and ('pass' in v.lower() or 'password' in v.lower())})
163
184
  if input_elem and input_elem.has_attr('name'):
164
185
  password_field = input_elem['name']
165
186
  info("Password field name identified: " + password_field)
187
+ debug(f"Password field detected: {password_field}")
166
188
  except Exception as e:
167
- # narrow catch so real errors bubble up elsewhere
168
189
  info(f"Password-field detection error: {e}")
190
+ debug("Password-field detection error raised.")
169
191
 
170
- # If we have a password to submit and a field to submit to, post it.
171
192
  if password and password_field:
172
193
  info("Using Password: " + password)
194
+ debug("Submitting password via POST.")
173
195
  post_headers = {'User-Agent': shared_state.values["user_agent"],
174
196
  'Content-Type': 'application/x-www-form-urlencoded'}
175
197
  data = {password_field: password}
176
198
  try:
177
199
  output = session.post(output.url, data=data, headers=post_headers, timeout=30)
200
+ debug("Password POST request successful.")
178
201
  except requests.RequestException as e:
179
202
  info(f"POSTing password failed: {e}")
203
+ debug("Password POST request failed.")
180
204
  return False
181
205
 
182
- # After posting, Cloudflare could reappear; ensure still bypassed
183
206
  if output.status_code == 403 or is_cloudflare_challenge(output.text):
184
207
  info("Encountered Cloudflare after password POST. Re-running FlareSolverr...")
208
+ debug("Cloudflare reappeared after password submit, retrying bypass.")
185
209
  session, headers, output = ensure_session_cf_bypassed(info, shared_state, session, output.url, headers)
186
210
  if not session or not output:
211
+ debug("Cloudflare bypass failed after password POST.")
187
212
  return False
188
213
 
189
- else:
190
- pass
191
-
192
214
  url = output.url
193
215
  soup = BeautifulSoup(output.text, 'html.parser')
216
+ debug("Re-parsed HTML after password submit or initial load.")
217
+
194
218
  if bool(soup.find_all("input", {"id": "p4assw0rt"})):
195
219
  info(f"Password was wrong or missing. Could not get links for {title}")
220
+ debug("Incorrect password detected via p4assw0rt.")
196
221
  return False
197
222
 
198
223
  no_captcha_present = bool(soup.find("form", {"class": "cnlform"}))
199
224
  if no_captcha_present:
200
225
  info("No CAPTCHA present. Skipping token!")
226
+ debug("Detected no CAPTCHA (CNL direct form).")
201
227
  else:
202
228
  circle_captcha = bool(soup.find_all("div", {"class": "circle_captcha"}))
229
+ debug(f"Circle captcha present: {circle_captcha}")
203
230
  i = 0
204
231
  while circle_captcha and i < 3:
232
+ debug(f"Submitting fake circle captcha click attempt {i+1}.")
205
233
  random_x = str(random.randint(100, 200))
206
234
  random_y = str(random.randint(100, 200))
207
235
  output = session.post(url, data="buttonx.x=" + random_x + "&buttonx.y=" + random_y,
@@ -210,40 +238,56 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
210
238
  url = output.url
211
239
  soup = BeautifulSoup(output.text, 'html.parser')
212
240
  circle_captcha = bool(soup.find_all("div", {"class": "circle_captcha"}))
241
+ i += 1
242
+ debug(f"Circle captcha still present: {circle_captcha}")
213
243
 
244
+ debug("Submitting final CAPTCHA token.")
214
245
  output = session.post(url, data="cap_token=" + token, headers={'User-Agent': shared_state.values["user_agent"],
215
246
  'Content-Type': 'application/x-www-form-urlencoded'})
216
247
  url = output.url
217
248
 
218
249
  if "/404.html" in url:
219
250
  info("Filecrypt returned 404 - current IP is likely banned or the link is offline.")
251
+ debug("Detected Filecrypt 404 page.")
220
252
 
221
253
  soup = BeautifulSoup(output.text, 'html.parser')
254
+ debug("Parsed post-captcha response HTML.")
222
255
 
223
256
  solved = bool(soup.find_all("div", {"class": "container"}))
224
257
  if not solved:
225
258
  info("Token rejected by Filecrypt! Try another CAPTCHA to proceed...")
259
+ debug("Token rejected; no 'container' div found.")
226
260
  return False
227
261
  else:
262
+ debug("CAPTCHA token accepted by Filecrypt.")
263
+
228
264
  season_number = ""
229
265
  episode_number = ""
230
266
  episode_in_title = re.findall(r'.*\.s(\d{1,3})e(\d{1,3})\..*', title, re.IGNORECASE)
231
267
  season_in_title = re.findall(r'.*\.s(\d{1,3})\..*', title, re.IGNORECASE)
268
+ debug("Attempting episode/season number parsing from title.")
269
+
232
270
  if episode_in_title:
233
271
  try:
234
272
  season_number = str(int(episode_in_title[0][0]))
235
273
  episode_number = str(int(episode_in_title[0][1]))
274
+ debug(f"Detected S{season_number}E{episode_number} from title.")
236
275
  except:
276
+ debug("Failed parsing S/E numbers from title.")
237
277
  pass
238
278
  elif season_in_title:
239
279
  try:
240
280
  season_number = str(int(season_in_title[0]))
281
+ debug(f"Detected season {season_number} from title.")
241
282
  except:
283
+ debug("Failed parsing season number from title.")
242
284
  pass
243
285
 
244
286
  season = ""
245
287
  episode = ""
246
288
  tv_show_selector = soup.find("div", {"class": "dlpart"})
289
+ debug(f"TV show selector found: {bool(tv_show_selector)}")
290
+
247
291
  if tv_show_selector:
248
292
 
249
293
  season = "season="
@@ -253,41 +297,53 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
253
297
  try:
254
298
  if season_selection:
255
299
  season += str(season_number)
300
+ debug(f"Assigned season parameter: {season}")
256
301
  except:
302
+ debug("Failed assigning season parameter.")
257
303
  pass
258
304
 
259
305
  episode_selection = soup.find("div", {"id": "selbox_episode"})
260
306
  try:
261
307
  if episode_selection:
262
308
  episode += str(episode_number)
309
+ debug(f"Assigned episode parameter: {episode}")
263
310
  except:
311
+ debug("Failed assigning episode parameter.")
264
312
  pass
265
313
 
266
314
  if episode_number and not episode:
267
315
  info(f"Missing select for episode number {episode_number}! Expect undesired links in the output.")
316
+ debug("Episode number present but no episode selector container found.")
268
317
 
269
318
  links = []
270
319
 
271
320
  mirrors = []
272
321
  mirrors_available = soup.select("a[href*=mirror]")
322
+ debug(f"Mirrors available: {len(mirrors_available)}")
323
+
273
324
  if not mirror and mirrors_available:
274
325
  for mirror in mirrors_available:
275
326
  try:
276
327
  mirror_query = mirror.get("href").split("?")[1]
277
328
  base_url = url.split("?")[0] if "mirror" in url else url
278
329
  mirrors.append(f"{base_url}?{mirror_query}")
330
+ debug(f"Discovered mirror: {mirrors[-1]}")
279
331
  except IndexError:
332
+ debug("Mirror parsing failed due to missing '?'.")
280
333
  continue
281
334
  else:
282
335
  mirrors = [url]
336
+ debug("Using direct URL as only mirror.")
283
337
 
284
338
  for mirror in mirrors:
285
339
  if not len(mirrors) == 1:
340
+ debug(f"Loading mirror: {mirror}")
286
341
  output = session.get(mirror, headers=headers)
287
342
  url = output.url
288
343
  soup = BeautifulSoup(output.text, 'html.parser')
289
344
 
290
345
  try:
346
+ debug("Attempting Click'n'Load decrypt.")
291
347
  crypted_payload = soup.find("form", {"class": "cnlform"}).get('onsubmit')
292
348
  crypted_data = re.findall(r"'(.*?)'", crypted_payload)
293
349
  if not title:
@@ -298,7 +354,9 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
298
354
  crypted_data[2],
299
355
  title
300
356
  ]
357
+
301
358
  if episode and season:
359
+ debug("Applying episode/season filtering to CNL.")
302
360
  domain = urlparse(url).netloc
303
361
  filtered_cnl_secret = soup.find("input", {"name": "hidden_cnl_id"}).attrs["value"]
304
362
  filtered_cnl_link = f"https://{domain}/_CNL/{filtered_cnl_secret}.html?{season}&{episode}"
@@ -307,6 +365,7 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
307
365
  if filtered_cnl_result.status_code == 200:
308
366
  filtered_cnl_data = json.loads(filtered_cnl_result.text)
309
367
  if filtered_cnl_data["success"]:
368
+ debug("Season/Episode filter applied successfully.")
310
369
  crypted_data = [
311
370
  crypted_data[0],
312
371
  filtered_cnl_data["data"][0],
@@ -315,12 +374,15 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
315
374
  ]
316
375
  links.extend(CNL(crypted_data).decrypt())
317
376
  except:
377
+ debug("CNL decrypt failed; trying DLC fallback.")
318
378
  if "The owner of this folder has deactivated all hosts in this container in their settings." in soup.text:
319
379
  info(f"Mirror deactivated by the owner: {mirror}")
380
+ debug("Mirror deactivated detected in page text.")
320
381
  continue
321
382
 
322
383
  info("Click'n'Load not found! Falling back to DLC...")
323
384
  try:
385
+ debug("Attempting DLC fallback.")
324
386
  crypted_payload = soup.find("button", {"class": "dlcdownload"}).get("onclick")
325
387
  crypted_data = re.findall(r"'(.*?)'", crypted_payload)
326
388
  dlc_secret = crypted_data[0]
@@ -332,18 +394,20 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
332
394
  dlc_file = session.get(dlc_link, headers=headers).content
333
395
  links.extend(DLC(shared_state, dlc_file).decrypt())
334
396
  except:
397
+ debug("DLC fallback failed, trying button fallback.")
335
398
  info("DLC not found! Falling back to first available download Button...")
336
399
 
337
400
  base_url = urlparse(url).netloc
338
401
  phpsessid = session.cookies.get('PHPSESSID')
339
402
  if not phpsessid:
340
403
  info("PHPSESSID cookie not found! Cannot proceed with download links extraction.")
404
+ debug("Missing PHPSESSID cookie.")
341
405
  return False
342
406
 
343
407
  results = []
408
+ debug("Parsing fallback buttons for download links.")
344
409
 
345
410
  for button in soup.find_all('button'):
346
- # Find the correct data-* attribute (only one expected)
347
411
  data_attrs = [v for k, v in button.attrs.items() if k.startswith('data-') and k != 'data-i18n']
348
412
  if not data_attrs:
349
413
  continue
@@ -356,6 +420,7 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
356
420
  results.append((full_url, mirror_name))
357
421
 
358
422
  sorted_results = sorted(results, key=lambda x: 0 if 'rapidgator' in x[1].lower() else 1)
423
+ debug(f"Found {len(sorted_results)} fallback link candidates.")
359
424
 
360
425
  for result_url, mirror in sorted_results:
361
426
  info("You must solve circlecaptcha separately!")
@@ -369,8 +434,10 @@ def get_filecrypt_links(shared_state, token, title, url, password=None, mirror=N
369
434
 
370
435
  if not links:
371
436
  info("No links found in Filecrypt response!")
437
+ debug("Extraction completed but yielded no links.")
372
438
  return False
373
439
 
440
+ debug(f"Returning success with {len(links)} extracted links.")
374
441
  return {
375
442
  "status": "success",
376
443
  "links": links
@@ -8,7 +8,7 @@ import requests
8
8
 
9
9
 
10
10
  def get_version():
11
- return "1.16.10"
11
+ return "1.17.0"
12
12
 
13
13
 
14
14
  def get_latest_version():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quasarr
3
- Version: 1.16.10
3
+ Version: 1.17.0
4
4
  Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
5
5
  Home-page: https://github.com/rix1337/Quasarr
6
6
  Author: rix1337
@@ -1,14 +1,14 @@
1
1
  quasarr/__init__.py,sha256=_WoDFvqXXilQynsiPrY-SXyADy1OwhAjQkdaJFqqHo0,17873
2
2
  quasarr/api/__init__.py,sha256=9Y_DTNYsHeimrXL3mAli8OUg0zqo7QGLF2ft40d3R-c,6822
3
3
  quasarr/api/arr/__init__.py,sha256=HrzyavxsCmQkdV2SMqQSoJq3KgrsPBnQJdo5iyovmG8,16626
4
- quasarr/api/captcha/__init__.py,sha256=hvzMhwTjFC8250ave_6xpUel1P0cVIAxUlQAj6AH_cc,25293
4
+ quasarr/api/captcha/__init__.py,sha256=EGXuhsks87w6gbj6RbupXuxhOMD3G9dl4zwwJ_U_8ck,32982
5
5
  quasarr/api/config/__init__.py,sha256=0K7zqC9dt39Ul1RIJt0zNVdh1b9ARnfC6QFPa2D9FCw,819
6
6
  quasarr/api/sponsors_helper/__init__.py,sha256=kAZabPlplPYRG6Uw7ZHTk5uypualwvhs-NoTOjQhhhA,6369
7
7
  quasarr/api/statistics/__init__.py,sha256=NrBAjjHkIUE95HhPUGIfNqh2IqBqJ_zm00S90Y-Qnus,7038
8
8
  quasarr/downloads/__init__.py,sha256=rRAkr0Kpk30HOvZ2AFt1ujiVPV_-qf7MSN4ytsHb9jE,10488
9
9
  quasarr/downloads/linkcrypters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  quasarr/downloads/linkcrypters/al.py,sha256=pM3NDan8x0WU8OS1GV3HuuV4B6Nm0a-ATrVORvLHt9M,8487
11
- quasarr/downloads/linkcrypters/filecrypt.py,sha256=BsHJ4bY6DifP-TrNnlrw7lHt3ZfvozvU3YLtsqkkSao,14904
11
+ quasarr/downloads/linkcrypters/filecrypt.py,sha256=GT51x_MG_hW4IpOF6OvL5r-2mTnMijI8K7_1D5Bfn4U,18884
12
12
  quasarr/downloads/linkcrypters/hide.py,sha256=kMxjsYZJpC1V3jwYv9b0h4HKBIectLlgglwOmexvFXs,4107
13
13
  quasarr/downloads/packages/__init__.py,sha256=C-6b_IgKQsmQWo5onTqFqx2pCOvPkT0oH-7jiopa8Hk,16748
14
14
  quasarr/downloads/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -33,7 +33,7 @@ quasarr/providers/notifications.py,sha256=bohT-6yudmFnmZMc3BwCGX0n1HdzSVgQG_LDZm
33
33
  quasarr/providers/obfuscated.py,sha256=XfiEblJizqixUoA4MIsillr5Nh1dwFqCgLvBQWM7Lwo,193865
34
34
  quasarr/providers/shared_state.py,sha256=4nswf5AuA4c1DWqSXsX0HXwlDt5e-UUUvQSy-vryCRE,28987
35
35
  quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
36
- quasarr/providers/version.py,sha256=xVx9RxdXBxRzArNfQrcRg5S24I5oucvYbJlko4b7EkU,4005
36
+ quasarr/providers/version.py,sha256=G0ZVMRC6n_iv9xlSiAzj6jfWvXrCkN6rfF__BOiB5lw,4004
37
37
  quasarr/providers/web_server.py,sha256=XPj98T-axxgotovuB-rVw1IPCkJiNdXBlEeFvM_zSlM,1432
38
38
  quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  quasarr/providers/sessions/al.py,sha256=mlP6SWfCY2HyOSV40uyotQ5T4eSBNYG9A5GWOEAdz-c,9589
@@ -56,9 +56,9 @@ quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
56
  quasarr/storage/config.py,sha256=ISZXh9gHiBu5mYhHGYx8nZ8JyMYuSFqfVl52DiUDJec,5994
57
57
  quasarr/storage/setup.py,sha256=gpHOsc5qtt-M72saZoMJFLE2YlCrjv7FWZknh-iVKsk,17766
58
58
  quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
59
- quasarr-1.16.10.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
60
- quasarr-1.16.10.dist-info/METADATA,sha256=0oMRPK6KnhBc2jHNUcXWmN1bSdYeo4uqFPbbZTTXOns,12250
61
- quasarr-1.16.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
- quasarr-1.16.10.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
63
- quasarr-1.16.10.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
64
- quasarr-1.16.10.dist-info/RECORD,,
59
+ quasarr-1.17.0.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
60
+ quasarr-1.17.0.dist-info/METADATA,sha256=pXe5L01d2AqOa9kkhW4OhWNsuYyB1ZSplLwqNrnmkRI,12249
61
+ quasarr-1.17.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
+ quasarr-1.17.0.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
63
+ quasarr-1.17.0.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
64
+ quasarr-1.17.0.dist-info/RECORD,,