quasarr 2.6.0__py3-none-any.whl → 2.7.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.

Files changed (57) hide show
  1. quasarr/__init__.py +71 -61
  2. quasarr/api/__init__.py +3 -4
  3. quasarr/api/arr/__init__.py +159 -56
  4. quasarr/api/captcha/__init__.py +203 -154
  5. quasarr/api/config/__init__.py +1 -1
  6. quasarr/api/jdownloader/__init__.py +19 -12
  7. quasarr/downloads/__init__.py +12 -8
  8. quasarr/downloads/linkcrypters/al.py +3 -3
  9. quasarr/downloads/linkcrypters/filecrypt.py +1 -2
  10. quasarr/downloads/packages/__init__.py +62 -88
  11. quasarr/downloads/sources/al.py +3 -3
  12. quasarr/downloads/sources/by.py +3 -3
  13. quasarr/downloads/sources/he.py +8 -9
  14. quasarr/downloads/sources/nk.py +3 -3
  15. quasarr/downloads/sources/sl.py +6 -1
  16. quasarr/downloads/sources/wd.py +132 -62
  17. quasarr/downloads/sources/wx.py +11 -17
  18. quasarr/providers/auth.py +9 -13
  19. quasarr/providers/cloudflare.py +50 -4
  20. quasarr/providers/imdb_metadata.py +0 -2
  21. quasarr/providers/jd_cache.py +64 -90
  22. quasarr/providers/log.py +226 -8
  23. quasarr/providers/myjd_api.py +116 -94
  24. quasarr/providers/sessions/al.py +20 -22
  25. quasarr/providers/sessions/dd.py +1 -1
  26. quasarr/providers/sessions/dl.py +8 -10
  27. quasarr/providers/sessions/nx.py +1 -1
  28. quasarr/providers/shared_state.py +26 -15
  29. quasarr/providers/utils.py +15 -6
  30. quasarr/providers/version.py +1 -1
  31. quasarr/search/__init__.py +91 -78
  32. quasarr/search/sources/al.py +19 -23
  33. quasarr/search/sources/by.py +6 -6
  34. quasarr/search/sources/dd.py +8 -10
  35. quasarr/search/sources/dj.py +15 -18
  36. quasarr/search/sources/dl.py +25 -37
  37. quasarr/search/sources/dt.py +13 -15
  38. quasarr/search/sources/dw.py +24 -16
  39. quasarr/search/sources/fx.py +25 -11
  40. quasarr/search/sources/he.py +16 -14
  41. quasarr/search/sources/hs.py +7 -7
  42. quasarr/search/sources/mb.py +7 -7
  43. quasarr/search/sources/nk.py +24 -25
  44. quasarr/search/sources/nx.py +22 -15
  45. quasarr/search/sources/sf.py +18 -9
  46. quasarr/search/sources/sj.py +7 -7
  47. quasarr/search/sources/sl.py +26 -14
  48. quasarr/search/sources/wd.py +63 -9
  49. quasarr/search/sources/wx.py +33 -47
  50. quasarr/storage/config.py +1 -3
  51. quasarr/storage/setup.py +13 -4
  52. {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/METADATA +4 -1
  53. quasarr-2.7.0.dist-info/RECORD +84 -0
  54. quasarr-2.6.0.dist-info/RECORD +0 -84
  55. {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/WHEEL +0 -0
  56. {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/entry_points.txt +0 -0
  57. {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,7 +16,7 @@ from quasarr.downloads.linkcrypters.filecrypt import DLC, get_filecrypt_links
16
16
  from quasarr.downloads.packages import delete_package
17
17
  from quasarr.providers import obfuscated, shared_state
18
18
  from quasarr.providers.html_templates import render_button, render_centered_html
19
- from quasarr.providers.log import debug, info
19
+ from quasarr.providers.log import debug, error, info, trace
20
20
  from quasarr.providers.statistics import StatsHelper
21
21
 
22
22
 
@@ -212,37 +212,22 @@ def setup_captcha_routes(app):
212
212
  f"pkg_pass={quote(password)}"
213
213
  )
214
214
 
215
+ js_url = url_with_quick_transfer_params.replace("'", "\\'")
216
+ js_userscript_url = userscript_url.replace("'", "\\'")
217
+ js_provider_name = provider_name.replace("'", "\\'")
218
+
215
219
  return f'''
216
220
  <div>
217
- <!-- One-time setup section - visually separated -->
218
- <div id="setup-instructions" class="setup-box">
219
- <h3>📦 First Time Setup:</h3>
220
- <p style="margin-bottom: 8px;">
221
- <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. On mobile Safari/Firefox or any Desktop Browser install Tampermonkey</a>
222
- </p>
223
- <p style="margin-top: 0; margin-bottom: 8px;">
224
- <a href="{userscript_url}" target="_blank">2. Install the {provider_name} userscript</a>
225
- </p>
226
- <p style="margin-top: 0; margin-bottom: 12px;">
227
- 3. Open link, solve CAPTCHAs, and links are automatically sent back to Quasarr!
228
- </p>
229
- <p style="margin-top: 0;">
230
- <button id="hide-setup-btn" type="button" class="btn-subtle">
231
- ✅ Don't show this again
232
- </button>
233
- </p>
234
- </div>
235
-
236
- <!-- Hidden "show instructions" button -->
237
- <div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
238
- <button id="show-setup-btn" type="button" class="btn-subtle">
239
- ℹ️ Show setup instructions
240
- </button>
241
- </div>
242
-
243
221
  <!-- Primary action - the quick transfer link -->
244
222
  <p>
245
- {render_button(f"Open {provider_name} & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
223
+ {render_button(f"Open {provider_name} & Get Download Links", "primary", {"onclick": f"handleProviderClick('{js_url}', '{storage_key}', '{js_provider_name}', '{js_userscript_url}')"})}
224
+ </p>
225
+
226
+ <!-- Reset tutorial button -->
227
+ <p id="reset-tutorial-btn" style="display: none;">
228
+ <button type="button" class="btn-subtle" onclick="localStorage.removeItem('{storage_key}'); showModal('Tutorial Reset', '<p>Tutorial reset! Click the Open button to see it again.</p>', '<button class=\\'btn-primary\\' onclick=\\'location.reload()\\'>Reload</button>');">
229
+ ℹ️ Reset Setup Guide
230
+ </button>
246
231
  </p>
247
232
 
248
233
  <!-- Manual submission - collapsible -->
@@ -286,29 +271,60 @@ def setup_captcha_routes(app):
286
271
  }});
287
272
  }}
288
273
 
289
- // Handle setup instructions hide/show
290
- const hideSetup = localStorage.getItem('{storage_key}');
291
- const setupBox = document.getElementById('setup-instructions');
292
- const showLink = document.getElementById('show-instructions-link');
293
-
294
- if (hideSetup === 'true') {{
295
- setupBox.style.display = 'none';
296
- showLink.style.display = 'block';
274
+ // Show reset button if tutorial was already seen
275
+ if (localStorage.getItem('{storage_key}') === 'true') {{
276
+ document.getElementById('reset-tutorial-btn').style.display = 'block';
297
277
  }}
298
278
 
299
- // Hide setup instructions
300
- document.getElementById('hide-setup-btn').addEventListener('click', function() {{
301
- localStorage.setItem('{storage_key}', 'true');
302
- setupBox.style.display = 'none';
303
- showLink.style.display = 'block';
304
- }});
305
-
306
- // Show setup instructions again
307
- document.getElementById('show-setup-btn').addEventListener('click', function() {{
308
- localStorage.setItem('{storage_key}', 'false');
309
- setupBox.style.display = 'block';
310
- showLink.style.display = 'none';
311
- }});
279
+ // Global handler for provider clicks
280
+ if (!window.handleProviderClick) {{
281
+ window.handleProviderClick = function(url, storageKey, providerName, userscriptUrl) {{
282
+ if (localStorage.getItem(storageKey) === 'true') {{
283
+ if(typeof incrementCaptchaAttempts==='function') incrementCaptchaAttempts();
284
+ window.location.href = url;
285
+ return;
286
+ }}
287
+
288
+ const content = `
289
+ <p style="margin-bottom: 8px;">
290
+ <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. On mobile Safari/Firefox or any Desktop Browser install Tampermonkey</a>
291
+ </p>
292
+ <p style="margin-top: 0; margin-bottom: 8px;">
293
+ <a href="${{userscriptUrl}}" target="_blank">2. Install the ${{providerName}} userscript</a>
294
+ </p>
295
+ <p style="margin-top: 0; margin-bottom: 12px;">
296
+ 3. Open link, solve CAPTCHAs, and links are automatically sent back to Quasarr!
297
+ </p>
298
+ `;
299
+
300
+ const btnId = 'modal-proceed-btn-' + Math.floor(Math.random() * 10000);
301
+ const buttons = `
302
+ <button id="${{btnId}}" class="btn-primary" disabled>Wait 5s...</button>
303
+ <button class="btn-secondary" onclick="closeModal()">Cancel</button>
304
+ `;
305
+
306
+ showModal('📦 First Time Setup', content, buttons);
307
+
308
+ let count = 5;
309
+ const btn = document.getElementById(btnId);
310
+ const interval = setInterval(() => {{
311
+ count--;
312
+ if (count <= 0) {{
313
+ clearInterval(interval);
314
+ btn.innerText = 'I have installed Tampermonkey and the userscript';
315
+ btn.disabled = false;
316
+ btn.onclick = function() {{
317
+ localStorage.setItem(storageKey, 'true');
318
+ closeModal();
319
+ if(typeof incrementCaptchaAttempts==='function') incrementCaptchaAttempts();
320
+ window.location.href = url;
321
+ }};
322
+ }} else {{
323
+ btn.innerText = 'Wait ' + count + 's...';
324
+ }}
325
+ }}, 1000);
326
+ }};
327
+ }}
312
328
  </script>
313
329
  '''
314
330
 
@@ -547,40 +563,26 @@ def setup_captcha_routes(app):
547
563
  f"pkg_pass={quote(password)}"
548
564
  )
549
565
 
566
+ js_url = url_with_quick_transfer_params.replace("'", "\\'")
567
+ storage_key = "hideFileCryptSetupInstructions"
568
+ provider_name = "FileCrypt"
569
+ userscript_url = "/captcha/filecrypt.user.js"
570
+
550
571
  return f'''
551
572
  <div class="section-divider" style="max-width: 370px; margin-left: auto; margin-right: auto;">
552
573
  <details id="bypassDetails">
553
574
  <summary id="bypassSummary">Show CAPTCHA Bypass</summary><br>
554
575
 
555
- <!-- One-time setup section - visually separated -->
556
- <div id="setup-instructions" class="setup-box">
557
- <h3>📦 First Time Setup:</h3>
558
- <p style="margin-bottom: 8px;">
559
- <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. On mobile Safari/Firefox or any Desktop Browser install Tampermonkey</a>
560
- </p>
561
- <p style="margin-top: 0; margin-bottom: 8px;">
562
- <a href="/captcha/filecrypt.user.js" target="_blank">2. Install the FileCrypt userscript</a>
563
- </p>
564
- <p style="margin-top: 0; margin-bottom: 12px;">
565
- 3. Open link, solve CAPTCHAs, and links are automatically sent back to Quasarr!
566
- </p>
567
- <p style="margin-top: 0;">
568
- <button id="hide-setup-btn" type="button" class="btn-subtle">
569
- ✅ Don't show this again
570
- </button>
571
- </p>
572
- </div>
573
-
574
- <!-- Hidden "show instructions" button -->
575
- <div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
576
- <button id="show-setup-btn" type="button" class="btn-subtle">
577
- ℹ️ Show setup instructions
578
- </button>
579
- </div>
580
-
581
576
  <!-- Primary action button -->
582
577
  <p>
583
- {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
578
+ {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"handleProviderClick('{js_url}', '{storage_key}', '{provider_name}', '{userscript_url}')"})}
579
+ </p>
580
+
581
+ <!-- Reset tutorial button -->
582
+ <p id="reset-tutorial-btn" style="display: none;">
583
+ <button type="button" class="btn-subtle" onclick="localStorage.removeItem('{storage_key}'); showModal('Tutorial Reset', '<p>Tutorial reset! Click the Open button to see it again.</p>', '<button class=\\'btn-primary\\' onclick=\\'location.reload()\\'>Reload</button>');">
584
+ ℹ️ Reset Setup Guide
585
+ </button>
584
586
  </p>
585
587
 
586
588
  <!-- Manual submission section -->
@@ -625,29 +627,60 @@ def setup_captcha_routes(app):
625
627
  }});
626
628
  }}
627
629
 
628
- // Handle setup instructions hide/show
629
- const hideSetup = localStorage.getItem('hideFileCryptSetupInstructions');
630
- const setupBox = document.getElementById('setup-instructions');
631
- const showLink = document.getElementById('show-instructions-link');
632
-
633
- if (hideSetup === 'true') {{
634
- setupBox.style.display = 'none';
635
- showLink.style.display = 'block';
630
+ // Show reset button if tutorial was already seen
631
+ if (localStorage.getItem('{storage_key}') === 'true') {{
632
+ document.getElementById('reset-tutorial-btn').style.display = 'block';
636
633
  }}
637
634
 
638
- // Hide setup instructions
639
- document.getElementById('hide-setup-btn').addEventListener('click', function() {{
640
- localStorage.setItem('hideFileCryptSetupInstructions', 'true');
641
- setupBox.style.display = 'none';
642
- showLink.style.display = 'block';
643
- }});
644
-
645
- // Show setup instructions again
646
- document.getElementById('show-setup-btn').addEventListener('click', function() {{
647
- localStorage.setItem('hideFileCryptSetupInstructions', 'false');
648
- setupBox.style.display = 'block';
649
- showLink.style.display = 'none';
650
- }});
635
+ // Global handler for provider clicks (if not already defined)
636
+ if (!window.handleProviderClick) {{
637
+ window.handleProviderClick = function(url, storageKey, providerName, userscriptUrl) {{
638
+ if (localStorage.getItem(storageKey) === 'true') {{
639
+ if(typeof incrementCaptchaAttempts==='function') incrementCaptchaAttempts();
640
+ window.location.href = url;
641
+ return;
642
+ }}
643
+
644
+ const content = `
645
+ <p style="margin-bottom: 8px;">
646
+ <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. On mobile Safari/Firefox or any Desktop Browser install Tampermonkey</a>
647
+ </p>
648
+ <p style="margin-top: 0; margin-bottom: 8px;">
649
+ <a href="${{userscriptUrl}}" target="_blank">2. Install the ${{providerName}} userscript</a>
650
+ </p>
651
+ <p style="margin-top: 0; margin-bottom: 12px;">
652
+ 3. Open link, solve CAPTCHAs, and links are automatically sent back to Quasarr!
653
+ </p>
654
+ `;
655
+
656
+ const btnId = 'modal-proceed-btn-' + Math.floor(Math.random() * 10000);
657
+ const buttons = `
658
+ <button id="${{btnId}}" class="btn-primary" disabled>Wait 5s...</button>
659
+ <button class="btn-secondary" onclick="closeModal()">Cancel</button>
660
+ `;
661
+
662
+ showModal('📦 First Time Setup', content, buttons);
663
+
664
+ let count = 5;
665
+ const btn = document.getElementById(btnId);
666
+ const interval = setInterval(() => {{
667
+ count--;
668
+ if (count <= 0) {{
669
+ clearInterval(interval);
670
+ btn.innerText = 'I have installed Tampermonkey and the userscript';
671
+ btn.disabled = false;
672
+ btn.onclick = function() {{
673
+ localStorage.setItem(storageKey, 'true');
674
+ closeModal();
675
+ if(typeof incrementCaptchaAttempts==='function') incrementCaptchaAttempts();
676
+ window.location.href = url;
677
+ }};
678
+ }} else {{
679
+ btn.innerText = 'Wait ' + count + 's...';
680
+ }}
681
+ }}, 1000);
682
+ }};
683
+ }}
651
684
  </script>
652
685
  '''
653
686
 
@@ -735,7 +768,7 @@ def setup_captcha_routes(app):
735
768
 
736
769
  selected = "selected" if pkg_id == current_package_id else ""
737
770
  # Truncate long titles for display
738
- display_title = (title[:50] + "...") if len(title) > 53 else title
771
+ display_title = title
739
772
  options.append(
740
773
  f'<option value="{captcha_type}|{quote(encoded)}" {selected}>{display_title}</option>'
741
774
  )
@@ -745,7 +778,7 @@ def setup_captcha_routes(app):
745
778
  return f"""
746
779
  <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;">
747
780
  <label for="package-select" style="display: block; margin-bottom: 8px; font-weight: bold;">📦 Select Package:</label>
748
- <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;">
781
+ <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; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">
749
782
  {options_html}
750
783
  </select>
751
784
  </div>
@@ -885,6 +918,11 @@ def setup_captcha_routes(app):
885
918
  if original_url:
886
919
  source_button = f"<p>{render_button('Source', 'secondary', {'onclick': f"window.open('{js_single_quoted_string_safe(original_url)}', '_blank')"})}</p>"
887
920
 
921
+ js_url = url_with_quick_transfer_params.replace("'", "\\'")
922
+ storage_key = "hideFileCryptFallbackSetupInstructions"
923
+ provider_name = "FileCrypt"
924
+ userscript_url = "/captcha/filecrypt.user.js"
925
+
888
926
  return render_centered_html(f"""
889
927
  <!DOCTYPE html>
890
928
  <html>
@@ -894,35 +932,16 @@ def setup_captcha_routes(app):
894
932
  {failed_warning}
895
933
 
896
934
  <div>
897
- <!-- One-time setup section - visually separated -->
898
- <div id="setup-instructions" class="setup-box">
899
- <h3>📦 First Time Setup:</h3>
900
- <p style="margin-bottom: 8px;">
901
- <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. On mobile Safari/Firefox or any Desktop Browser install Tampermonkey</a>
902
- </p>
903
- <p style="margin-top: 0; margin-bottom: 8px;">
904
- <a href="/captcha/filecrypt.user.js" target="_blank">2. Install the FileCrypt userscript</a>
905
- </p>
906
- <p style="margin-top: 0; margin-bottom: 12px;">
907
- 3. Open link, solve CAPTCHAs, and links are automatically sent back to Quasarr!
908
- </p>
909
- <p style="margin-top: 0;">
910
- <button id="hide-setup-btn" type="button" class="btn-subtle">
911
- ✅ Don't show this again
912
- </button>
913
- </p>
914
- </div>
915
-
916
- <!-- Hidden "show instructions" button -->
917
- <div id="show-instructions-link" style="display: none; margin-bottom: 16px;">
918
- <button id="show-setup-btn" type="button" class="btn-subtle">
919
- ℹ️ Show setup instructions
920
- </button>
921
- </div>
922
-
923
935
  <!-- Primary action button -->
924
936
  <p>
925
- {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"if(typeof incrementCaptchaAttempts==='function')incrementCaptchaAttempts();location.href='{url_with_quick_transfer_params}'"})}
937
+ {render_button("Open FileCrypt & Get Download Links", "primary", {"onclick": f"handleProviderClick('{js_url}', '{storage_key}', '{provider_name}', '{userscript_url}')"})}
938
+ </p>
939
+
940
+ <!-- Reset tutorial button -->
941
+ <p id="reset-tutorial-btn" style="display: none;">
942
+ <button type="button" class="btn-subtle" onclick="localStorage.removeItem('{storage_key}'); showModal('Tutorial Reset', '<p>Tutorial reset! Click the Open button to see it again.</p>', '<button class=\\'btn-primary\\' onclick=\\'location.reload()\\'>Reload</button>');">
943
+ ℹ️ Reset Setup Guide
944
+ </button>
926
945
  </p>
927
946
 
928
947
  <!-- Manual submission section -->
@@ -980,29 +999,58 @@ def setup_captcha_routes(app):
980
999
  }});
981
1000
  }}
982
1001
 
983
- // Handle setup instructions hide/show
984
- const hideSetup = localStorage.getItem('hideFileCryptFallbackSetupInstructions');
985
- const setupBox = document.getElementById('setup-instructions');
986
- const showLink = document.getElementById('show-instructions-link');
987
-
988
- if (hideSetup === 'true') {{
989
- setupBox.style.display = 'none';
990
- showLink.style.display = 'block';
1002
+ // Show reset button if tutorial was already seen
1003
+ if (localStorage.getItem('{storage_key}') === 'true') {{
1004
+ document.getElementById('reset-tutorial-btn').style.display = 'block';
991
1005
  }}
992
1006
 
993
- // Hide setup instructions
994
- document.getElementById('hide-setup-btn').addEventListener('click', function() {{
995
- localStorage.setItem('hideFileCryptFallbackSetupInstructions', 'true');
996
- setupBox.style.display = 'none';
997
- showLink.style.display = 'block';
998
- }});
999
-
1000
- // Show setup instructions again
1001
- document.getElementById('show-setup-btn').addEventListener('click', function() {{
1002
- localStorage.setItem('hideFileCryptFallbackSetupInstructions', 'false');
1003
- setupBox.style.display = 'block';
1004
- showLink.style.display = 'none';
1005
- }});
1007
+ // Global handler for provider clicks
1008
+ window.handleProviderClick = function(url, storageKey, providerName, userscriptUrl) {{
1009
+ if (localStorage.getItem(storageKey) === 'true') {{
1010
+ if(typeof incrementCaptchaAttempts==='function') incrementCaptchaAttempts();
1011
+ window.location.href = url;
1012
+ return;
1013
+ }}
1014
+
1015
+ const content = `
1016
+ <p style="margin-bottom: 8px;">
1017
+ <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener noreferrer">1. On mobile Safari/Firefox or any Desktop Browser install Tampermonkey</a>
1018
+ </p>
1019
+ <p style="margin-top: 0; margin-bottom: 8px;">
1020
+ <a href="${{userscriptUrl}}" target="_blank">2. Install the ${{providerName}} userscript</a>
1021
+ </p>
1022
+ <p style="margin-top: 0; margin-bottom: 12px;">
1023
+ 3. Open link, solve CAPTCHAs, and links are automatically sent back to Quasarr!
1024
+ </p>
1025
+ `;
1026
+
1027
+ const btnId = 'modal-proceed-btn-' + Math.floor(Math.random() * 10000);
1028
+ const buttons = `
1029
+ <button id="${{btnId}}" class="btn-primary" disabled>Wait 5s...</button>
1030
+ <button class="btn-secondary" onclick="closeModal()">Cancel</button>
1031
+ `;
1032
+
1033
+ showModal('📦 First Time Setup', content, buttons);
1034
+
1035
+ let count = 5;
1036
+ const btn = document.getElementById(btnId);
1037
+ const interval = setInterval(() => {{
1038
+ count--;
1039
+ if (count <= 0) {{
1040
+ clearInterval(interval);
1041
+ btn.innerText = 'I have installed Tampermonkey and the userscript';
1042
+ btn.disabled = false;
1043
+ btn.onclick = function() {{
1044
+ localStorage.setItem(storageKey, 'true');
1045
+ closeModal();
1046
+ if(typeof incrementCaptchaAttempts==='function') incrementCaptchaAttempts();
1047
+ window.location.href = url;
1048
+ }};
1049
+ }} else {{
1050
+ btn.innerText = 'Wait ' + count + 's...';
1051
+ }}
1052
+ }}, 1000);
1053
+ }};
1006
1054
  </script>
1007
1055
 
1008
1056
  </body>
@@ -1046,7 +1094,7 @@ def setup_captcha_routes(app):
1046
1094
  decoded, -15
1047
1095
  ) # -15 = raw deflate, no zlib header
1048
1096
  except Exception as e:
1049
- debug(f"Decompression error: {e}, trying with header...")
1097
+ trace(f"Decompression error: {e}, trying with header...")
1050
1098
  try:
1051
1099
  # Fallback: try with zlib header
1052
1100
  decompressed = zlib.decompress(decoded)
@@ -1070,7 +1118,9 @@ def setup_captcha_routes(app):
1070
1118
  link = "https://" + link
1071
1119
  links.append(link)
1072
1120
 
1073
- info(f"Quick transfer received {len(links)} links for package {package_id}")
1121
+ debug(
1122
+ f"Quick transfer received {len(links)} links for package {package_id}"
1123
+ )
1074
1124
 
1075
1125
  # Get package info
1076
1126
  raw_data = shared_state.get_db("protected").retrieve(package_id)
@@ -1095,7 +1145,7 @@ def setup_captcha_routes(app):
1095
1145
  StatsHelper(shared_state).increment_captcha_decryptions_manual()
1096
1146
  shared_state.get_db("protected").delete(package_id)
1097
1147
 
1098
- info(f"Quick transfer successful: {len(links)} links processed")
1148
+ info(f"Quick transfer successful: <g>{len(links)}</g> links processed")
1099
1149
 
1100
1150
  # Check if more CAPTCHAs remain
1101
1151
  remaining_protected = shared_state.get_db(
@@ -1131,7 +1181,7 @@ def setup_captcha_routes(app):
1131
1181
  </p>''')
1132
1182
 
1133
1183
  except Exception as e:
1134
- info(f"Quick transfer error: {e}")
1184
+ error(f"Quick transfer error: {e}")
1135
1185
  return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
1136
1186
  <p><b>Error:</b> {str(e)}</p>
1137
1187
  <p>
@@ -1656,14 +1706,13 @@ def setup_captcha_routes(app):
1656
1706
  mirror = None if (mirror := data.get("mirror")) == "None" else mirror
1657
1707
 
1658
1708
  if token:
1659
- info(f"Received token: {token}")
1660
- info(f"Decrypting links for {title}")
1709
+ info(f"Received token: {token} to decrypt links for {title}")
1661
1710
  decrypted = get_filecrypt_links(
1662
1711
  shared_state, token, title, link, password=password, mirror=mirror
1663
1712
  )
1664
1713
  if decrypted:
1665
1714
  links = decrypted.get("links", [])
1666
- info(f"Decrypted {len(links)} download links for {title}")
1715
+ info(f"Decrypted <g>{len(links)}</g> download links for {title}")
1667
1716
  if not links:
1668
1717
  raise ValueError("No download links found after decryption")
1669
1718
  downloaded = shared_state.download_package(
@@ -7,7 +7,7 @@ import signal
7
7
  import threading
8
8
  import time
9
9
 
10
- from bottle import request, response
10
+ from bottle import response
11
11
 
12
12
  from quasarr.providers.html_templates import render_button, render_form
13
13
  from quasarr.providers.log import info
@@ -16,9 +16,14 @@ def get_jdownloader_status(shared_state):
16
16
 
17
17
  jd_config = Config("JDownloader")
18
18
  jd_device = jd_config.get("device") or ""
19
-
19
+
20
20
  dev_name = jd_device if jd_device else "JDownloader"
21
- dev_name_safe = dev_name.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
21
+ dev_name_safe = (
22
+ dev_name.replace("&", "&amp;")
23
+ .replace("<", "&lt;")
24
+ .replace(">", "&gt;")
25
+ .replace('"', "&quot;")
26
+ )
22
27
 
23
28
  if jd_connected:
24
29
  status_text = f"✅ {dev_name_safe} connected"
@@ -34,7 +39,7 @@ def get_jdownloader_status(shared_state):
34
39
  "connected": jd_connected,
35
40
  "device_name": jd_device,
36
41
  "status_text": status_text,
37
- "status_class": status_class
42
+ "status_class": status_class,
38
43
  }
39
44
 
40
45
 
@@ -44,7 +49,7 @@ def get_jdownloader_modal_script():
44
49
  jd_user = jd_config.get("user") or ""
45
50
  jd_pass = jd_config.get("password") or ""
46
51
  jd_device = jd_config.get("device") or ""
47
-
52
+
48
53
  jd_user_js = jd_user.replace("\\", "\\\\").replace("'", "\\'")
49
54
  jd_pass_js = jd_pass.replace("\\", "\\\\").replace("'", "\\'")
50
55
  jd_device_js = jd_device.replace("\\", "\\\\").replace("'", "\\'")
@@ -166,13 +171,13 @@ def get_jdownloader_modal_script():
166
171
  def get_jdownloader_status_pill(shared_state):
167
172
  """Return the HTML for the JDownloader status pill."""
168
173
  status = get_jdownloader_status(shared_state)
169
-
174
+
170
175
  return f"""
171
- <span class="status-pill {status['status_class']}"
176
+ <span class="status-pill {status["status_class"]}"
172
177
  onclick="openJDownloaderModal()"
173
178
  style="cursor: pointer;"
174
179
  title="Click to configure JDownloader">
175
- {status['status_text']}
180
+ {status["status_text"]}
176
181
  </span>
177
182
  """
178
183
 
@@ -181,12 +186,14 @@ def get_jdownloader_disconnected_page(shared_state, back_url="/"):
181
186
  """Return a full error page when JDownloader is disconnected."""
182
187
  import quasarr.providers.html_images as images
183
188
  from quasarr.providers.html_templates import render_centered_html
184
-
189
+
185
190
  status_pill = get_jdownloader_status_pill(shared_state)
186
191
  modal_script = get_jdownloader_modal_script()
187
-
188
- back_btn = render_button("Back", "secondary", {"onclick": f"location.href='{back_url}'"})
189
-
192
+
193
+ back_btn = render_button(
194
+ "Back", "secondary", {"onclick": f"location.href='{back_url}'"}
195
+ )
196
+
190
197
  content = f'''
191
198
  <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
192
199
  <div class="status-bar">
@@ -228,5 +235,5 @@ def get_jdownloader_disconnected_page(shared_state, back_url="/"):
228
235
  </style>
229
236
  {modal_script}
230
237
  '''
231
-
238
+
232
239
  return render_centered_html(content)
@@ -26,7 +26,7 @@ from quasarr.downloads.sources.sl import get_sl_download_links
26
26
  from quasarr.downloads.sources.wd import get_wd_download_links
27
27
  from quasarr.downloads.sources.wx import get_wx_download_links
28
28
  from quasarr.providers.hostname_issues import clear_hostname_issue, mark_hostname_issue
29
- from quasarr.providers.log import info
29
+ from quasarr.providers.log import info, warn
30
30
  from quasarr.providers.notifications import send_discord_message
31
31
  from quasarr.providers.statistics import StatsHelper
32
32
  from quasarr.providers.utils import filter_offline_links
@@ -214,7 +214,7 @@ def handle_auto_decrypt_links(shared_state, links, title, password, package_id):
214
214
  if not decrypted_urls:
215
215
  return {"success": False, "reason": "No links decrypted"}
216
216
 
217
- info(f"Decrypted {len(decrypted_urls)} download links for {title}")
217
+ info(f"Decrypted <g>{len(decrypted_urls)}</g> download links for {title}")
218
218
 
219
219
  if shared_state.download_package(decrypted_urls, title, password, package_id):
220
220
  StatsHelper(shared_state).increment_package_with_links(decrypted_urls)
@@ -239,7 +239,7 @@ def store_protected_links(
239
239
  package_id, json.dumps(blob_data)
240
240
  )
241
241
  info(
242
- f'CAPTCHA-Solution required for "{title}" at: "{shared_state.values["external_address"]}/captcha"'
242
+ f'CAPTCHA-Solution required for <b>{title}</b> at: "{shared_state.values["external_address"]}/captcha"'
243
243
  )
244
244
  return {"success": True}
245
245
 
@@ -305,7 +305,9 @@ def process_links(
305
305
 
306
306
  # PRIORITY 1: Direct hoster links
307
307
  if classified["direct"]:
308
- info(f"Found {len(classified['direct'])} direct hoster links for {title}")
308
+ info(
309
+ f"Found <g>{len(classified['direct'])}</g> direct hoster links for {title}"
310
+ )
309
311
  send_discord_message(
310
312
  shared_state,
311
313
  title=title,
@@ -322,7 +324,9 @@ def process_links(
322
324
 
323
325
  # PRIORITY 2: Auto-decryptable (hide.cx)
324
326
  if classified["auto"]:
325
- info(f"Found {len(classified['auto'])} auto-decryptable links for {title}")
327
+ info(
328
+ f"Found <g>{len(classified['auto'])}</g> auto-decryptable links for {title}"
329
+ )
326
330
  result = handle_auto_decrypt_links(
327
331
  shared_state, classified["auto"], title, password, package_id
328
332
  )
@@ -340,7 +344,7 @@ def process_links(
340
344
 
341
345
  # PRIORITY 3: Protected (filecrypt, tolink, keeplinks, junkies)
342
346
  if classified["protected"]:
343
- info(f"Found {len(classified['protected'])} protected links for {title}")
347
+ info(f"Found <g>{len(classified['protected'])}</g> protected links for {title}")
344
348
  send_discord_message(
345
349
  shared_state,
346
350
  title=title,
@@ -363,7 +367,7 @@ def process_links(
363
367
  title,
364
368
  package_id,
365
369
  shared_state,
366
- reason=f'No usable links found for "{title}" on {label} - "{source_url}"',
370
+ reason=f'No usable links found for {title} on {label} - "{source_url}"',
367
371
  )
368
372
 
369
373
 
@@ -461,7 +465,7 @@ def download(
461
465
 
462
466
  # Skip Download if package_id already exists
463
467
  if package_id_exists(shared_state, package_id):
464
- info(f"Package {package_id} already exists. Skipping download!")
468
+ warn(f"Package {package_id} already exists. Skipping download!")
465
469
  return {"success": True, "package_id": package_id, "title": title}
466
470
 
467
471
  if source_result is None: