quasarr 2.4.11__tar.gz → 2.6.0__tar.gz

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 (99) hide show
  1. {quasarr-2.4.11 → quasarr-2.6.0}/.github/workflows/PullRequests.yml +17 -10
  2. {quasarr-2.4.11 → quasarr-2.6.0}/.github/workflows/Release.yml +8 -2
  3. {quasarr-2.4.11 → quasarr-2.6.0}/PKG-INFO +1 -1
  4. {quasarr-2.4.11 → quasarr-2.6.0}/pre-commit.py +40 -27
  5. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/__init__.py +30 -35
  6. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/__init__.py +23 -15
  7. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/arr/__init__.py +15 -6
  8. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/captcha/__init__.py +2 -9
  9. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/config/__init__.py +31 -168
  10. quasarr-2.6.0/quasarr/api/jdownloader/__init__.py +232 -0
  11. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/packages/__init__.py +2 -12
  12. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/__init__.py +2 -0
  13. quasarr-2.6.0/quasarr/downloads/sources/hs.py +131 -0
  14. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/html_templates.py +14 -3
  15. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/sessions/al.py +4 -0
  16. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/shared_state.py +17 -17
  17. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/version.py +1 -1
  18. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/__init__.py +90 -15
  19. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/al.py +17 -13
  20. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/by.py +4 -1
  21. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/dd.py +16 -4
  22. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/dl.py +13 -1
  23. quasarr-2.6.0/quasarr/search/sources/hs.py +515 -0
  24. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/mb.py +1 -7
  25. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/nx.py +4 -1
  26. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/wd.py +4 -1
  27. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/wx.py +10 -8
  28. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/storage/config.py +1 -0
  29. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/storage/setup.py +564 -266
  30. {quasarr-2.4.11 → quasarr-2.6.0}/.github/FUNDING.yml +0 -0
  31. {quasarr-2.4.11 → quasarr-2.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  32. {quasarr-2.4.11 → quasarr-2.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  33. {quasarr-2.4.11 → quasarr-2.6.0}/.github/workflows/HostnameRedaction.yml +0 -0
  34. {quasarr-2.4.11 → quasarr-2.6.0}/.gitignore +0 -0
  35. {quasarr-2.4.11 → quasarr-2.6.0}/.pre-commit-config.yaml +0 -0
  36. {quasarr-2.4.11 → quasarr-2.6.0}/CONTRIBUTING.md +0 -0
  37. {quasarr-2.4.11 → quasarr-2.6.0}/LICENSE +0 -0
  38. {quasarr-2.4.11 → quasarr-2.6.0}/Quasarr.png +0 -0
  39. {quasarr-2.4.11 → quasarr-2.6.0}/Quasarr.py +0 -0
  40. {quasarr-2.4.11 → quasarr-2.6.0}/README.md +0 -0
  41. {quasarr-2.4.11 → quasarr-2.6.0}/docker/Dockerfile +0 -0
  42. {quasarr-2.4.11 → quasarr-2.6.0}/docker/dev-services-compose.yml +0 -0
  43. {quasarr-2.4.11 → quasarr-2.6.0}/docker/docker-compose.yml +0 -0
  44. {quasarr-2.4.11 → quasarr-2.6.0}/pyproject.toml +0 -0
  45. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/sponsors_helper/__init__.py +0 -0
  46. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/api/statistics/__init__.py +0 -0
  47. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/linkcrypters/__init__.py +0 -0
  48. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/linkcrypters/al.py +0 -0
  49. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
  50. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/linkcrypters/hide.py +0 -0
  51. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/packages/__init__.py +0 -0
  52. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/__init__.py +0 -0
  53. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/al.py +0 -0
  54. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/by.py +0 -0
  55. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/dd.py +0 -0
  56. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/dj.py +0 -0
  57. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/dl.py +0 -0
  58. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/dt.py +0 -0
  59. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/dw.py +0 -0
  60. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/he.py +0 -0
  61. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/mb.py +0 -0
  62. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/nk.py +0 -0
  63. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/nx.py +0 -0
  64. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/sf.py +0 -0
  65. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/sj.py +0 -0
  66. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/sl.py +0 -0
  67. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/wd.py +0 -0
  68. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/downloads/sources/wx.py +0 -0
  69. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/__init__.py +0 -0
  70. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/auth.py +0 -0
  71. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/cloudflare.py +0 -0
  72. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/hostname_issues.py +0 -0
  73. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/html_images.py +0 -0
  74. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/imdb_metadata.py +0 -0
  75. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/jd_cache.py +0 -0
  76. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/log.py +0 -0
  77. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/myjd_api.py +0 -0
  78. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/notifications.py +0 -0
  79. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/obfuscated.py +0 -0
  80. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/sessions/__init__.py +0 -0
  81. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/sessions/dd.py +0 -0
  82. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/sessions/dl.py +0 -0
  83. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/sessions/nx.py +0 -0
  84. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/statistics.py +0 -0
  85. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/utils.py +0 -0
  86. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/providers/web_server.py +0 -0
  87. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/__init__.py +0 -0
  88. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/dj.py +0 -0
  89. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/dt.py +0 -0
  90. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/dw.py +0 -0
  91. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/fx.py +0 -0
  92. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/he.py +0 -0
  93. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/nk.py +0 -0
  94. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/sf.py +0 -0
  95. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/sj.py +0 -0
  96. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/search/sources/sl.py +0 -0
  97. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/storage/__init__.py +0 -0
  98. {quasarr-2.4.11 → quasarr-2.6.0}/quasarr/storage/sqlite_database.py +0 -0
  99. {quasarr-2.4.11 → quasarr-2.6.0}/uv.lock +0 -0
@@ -53,10 +53,11 @@ jobs:
53
53
 
54
54
  version:
55
55
  needs: [ quality-check ]
56
- if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name) == 'dev'
56
+ if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
57
57
  runs-on: ubuntu-latest
58
58
  outputs:
59
59
  version: ${{ steps.version.outputs.version }}
60
+ epoch: ${{ steps.version.outputs.epoch }}
60
61
  steps:
61
62
  - uses: actions/checkout@v6
62
63
  - uses: actions/setup-python@v5
@@ -68,12 +69,14 @@ jobs:
68
69
  - id: version
69
70
  run: |
70
71
  VERSION=$(uv run python quasarr/providers/version.py)
72
+ EPOCH=$(date +%s)
71
73
  echo "version=$VERSION" >> $GITHUB_OUTPUT
72
- echo "🏷️ Detected version: $VERSION"
74
+ echo "epoch=$EPOCH" >> $GITHUB_OUTPUT
75
+ echo "🏷️ Detected version: $VERSION ($EPOCH)"
73
76
 
74
77
  build-wheel:
75
78
  needs: [ quality-check, version ]
76
- if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name) == 'dev'
79
+ if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
77
80
  runs-on: ubuntu-latest
78
81
  outputs:
79
82
  attestation-id: ${{ steps.attest.outputs.attestation-id }}
@@ -103,7 +106,7 @@ jobs:
103
106
 
104
107
  build-exe:
105
108
  needs: [ quality-check, version ]
106
- if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name) == 'dev'
109
+ if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
107
110
  runs-on: windows-latest
108
111
  env:
109
112
  TMP: "D:\\a\\temp"
@@ -168,10 +171,12 @@ jobs:
168
171
  - name: Create Release Body
169
172
  run: |
170
173
  echo "📝 Generating Release Body..."
171
- echo "### Docker:" > release_body.md
174
+ echo "# ${{ needs.version.outputs.version }} (${{ needs.version.outputs.epoch }})" > release_body.md
175
+ echo "" >> release_body.md
176
+ echo "### Docker:" >> release_body.md
172
177
  echo "\`docker pull ${{ env.GHCR_ENDPOINT }}:beta\`" >> release_body.md
173
178
  echo "### Python:" >> release_body.md
174
- echo "\`uv tool upgrade quasarr\`" >> release_body.md
179
+ echo "\`uv tool install https://github.com/rix1337/Quasarr/releases/download/beta/quasarr-${{ needs.version.outputs.version }}-py3-none-any.whl\`" >> release_body.md
175
180
  echo "### Changelog:" >> release_body.md
176
181
  echo "${{ steps.changelog.outputs.changelog }}" >> release_body.md
177
182
  echo "[Attestation](https://github.com/${{ github.repository }}/attestations/${{ needs.build-wheel.outputs.attestation-id }})" >> release_body.md
@@ -182,14 +187,14 @@ jobs:
182
187
  removeArtifacts: true
183
188
  replacesArtifacts: true
184
189
  tag: beta
185
- name: Beta Build
190
+ name: "${{ needs.version.outputs.version }} (${{ needs.version.outputs.epoch }})"
186
191
  bodyFile: "release_body.md"
187
192
  prerelease: true
188
193
  token: ${{ secrets.GITHUB_TOKEN }}
189
194
 
190
195
  build-docker-amd64:
191
196
  needs: [ quality-check, version, build-wheel ]
192
- if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name) == 'dev'
197
+ if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
193
198
  runs-on: ubuntu-latest
194
199
  steps:
195
200
  - uses: actions/checkout@v6
@@ -221,7 +226,7 @@ jobs:
221
226
 
222
227
  build-docker-arm64:
223
228
  needs: [ quality-check, version, build-wheel ]
224
- if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name) == 'dev'
229
+ if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
225
230
  runs-on: ubuntu-24.04-arm
226
231
  steps:
227
232
  - uses: actions/checkout@v6
@@ -292,12 +297,13 @@ jobs:
292
297
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
293
298
  DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
294
299
  VERSION: ${{ needs.version.outputs.version }}
300
+ EPOCH: ${{ needs.version.outputs.epoch }}
295
301
  REPO: ${{ github.repository }}
296
302
  run: |
297
303
  echo "📢 Notifying stakeholders..."
298
304
  RELEASE_BODY=$(gh release view beta --json body --jq .body)
299
305
  if [ -n "$DISCORD_WEBHOOK" ]; then
300
- jq -n --arg title "🚀 New Beta Build: $VERSION" --arg desc "$RELEASE_BODY" --arg url "https://github.com/$REPO/releases/tag/beta" '{content: null, embeds: [{title: $title, description: $desc, url: $url, color: 5814783}]}' > discord_payload.json
306
+ jq -n --arg title "🚀 New Beta Build: $VERSION ($EPOCH)" --arg url "https://github.com/$REPO/releases/tag/beta" '{content: null, flags: 4096, embeds: [{title: $title, url: $url, color: 5814783}]}' > discord_payload.json
301
307
  curl -H "Content-Type: application/json" -d @discord_payload.json "$DISCORD_WEBHOOK"
302
308
  fi
303
309
  PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
@@ -338,4 +344,5 @@ jobs:
338
344
  echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
339
345
  echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
340
346
  echo "| **Version** | \`${{ needs.version.outputs.version || 'N/A' }}\` |" >> $GITHUB_STEP_SUMMARY
347
+ echo "| **Epoch** | \`${{ needs.version.outputs.epoch || 'N/A' }}\` |" >> $GITHUB_STEP_SUMMARY
341
348
  echo "| **Release Job** | ${{ needs.beta-release.result || 'Skipped' }} |" >> $GITHUB_STEP_SUMMARY
@@ -254,18 +254,24 @@ jobs:
254
254
  uses: metcalfc/changelog-generator@v4.6.2
255
255
  with:
256
256
  myToken: ${{ secrets.GITHUB_TOKEN }}
257
- - name: Add PR Body to Changelog
257
+ - name: Get PR Body
258
258
  env:
259
259
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
260
260
  run: |
261
261
  PR_BODY=$(gh pr list --search "${{ github.sha }}" --state merged --json body --jq '.[0].body // empty')
262
- if [ -n "$PR_BODY" ]; then echo -e "\n$PR_BODY" >> .github/Changelog.md; fi
262
+ if [ -n "$PR_BODY" ]; then
263
+ echo "$PR_BODY" > pr_body.txt
264
+ fi
263
265
  - name: Create Release Body
264
266
  run: |
265
267
  echo "### Docker:" > release_body.md
266
268
  echo "\`docker pull ${{ env.GHCR_ENDPOINT }}:latest\`" >> release_body.md
267
269
  echo "### Python:" >> release_body.md
268
270
  echo "\`uv tool upgrade quasarr\`" >> release_body.md
271
+ if [ -f pr_body.txt ]; then
272
+ cat pr_body.txt >> release_body.md
273
+ echo "" >> release_body.md
274
+ fi
269
275
  echo "### Changelog:" >> release_body.md
270
276
  echo "${{ steps.changelog.outputs.changelog }}" >> release_body.md
271
277
  echo "[Attestation](https://github.com/${{ github.repository }}/attestations/${{ needs.build-wheel.outputs.attestation-id }})" >> release_body.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quasarr
3
- Version: 2.4.11
3
+ Version: 2.6.0
4
4
  Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
5
5
  Author-email: rix1337 <rix1337@users.noreply.github.com>
6
6
  License-File: LICENSE
@@ -11,9 +11,16 @@ VERSION_FILE = Path("quasarr/providers/version.py")
11
11
  PYPROJECT_FILE = Path("pyproject.toml")
12
12
 
13
13
 
14
+ def safe_print(msg):
15
+ try:
16
+ print(msg)
17
+ except Exception:
18
+ print(msg.encode("ascii", errors="replace").decode("ascii"))
19
+
20
+
14
21
  def run(cmd, check=True, capture=False, text=True):
15
22
  """Helper to run shell commands comfortably."""
16
- print(f"⚙️ Exec: {' '.join(cmd)}")
23
+ safe_print(f"⚙️ Exec: {' '.join(cmd)}")
17
24
  return subprocess.run(cmd, check=check, capture_output=capture, text=text)
18
25
 
19
26
 
@@ -29,29 +36,31 @@ def git_status_has_changes():
29
36
 
30
37
 
31
38
  def task_format():
32
- print("\n🔍 --- 1. FORMATTING & SYNTAX CHECK ---")
39
+ safe_print("\n🔍 --- 1. FORMATTING & SYNTAX CHECK ---")
33
40
 
34
41
  # Runs Ruff using the rules defined in pyproject.toml
35
42
  result = run(["uv", "run", "ruff", "check", "--fix", "."], check=False)
36
43
 
37
44
  if result.returncode != 0:
38
- print("❌ Critical errors or syntax issues found. Fix them before staging.")
45
+ safe_print(
46
+ "❌ Critical errors or syntax issues found. Fix them before staging."
47
+ )
39
48
  return False
40
49
 
41
50
  # Standard formatting (indentation/spacing)
42
51
  run(["uv", "run", "ruff", "format", "."], check=False)
43
52
 
44
53
  if git_status_has_changes():
45
- print("✅ Linting fixes applied and staged.")
54
+ safe_print("✅ Linting fixes applied and staged.")
46
55
  run(["git", "add", "."])
47
56
  return True
48
57
 
49
- print("✨ Code style is already perfect.")
58
+ safe_print("✨ Code style is already perfect.")
50
59
  return False
51
60
 
52
61
 
53
62
  def task_upgrade_deps():
54
- print("\n📦 --- 2. DEPENDENCIES ---")
63
+ safe_print("\n📦 --- 2. DEPENDENCIES ---")
55
64
  try:
56
65
  with open(PYPROJECT_FILE, "rb") as f:
57
66
  pyproj = tomllib.load(f)
@@ -65,7 +74,7 @@ def task_upgrade_deps():
65
74
  if deps:
66
75
  pkgs = [get_pkg_name(d) for d in deps if get_pkg_name(d)]
67
76
  if pkgs:
68
- print(f"⬆️ Upgrading main: {pkgs}")
77
+ safe_print(f"⬆️ Upgrading main: {pkgs}")
69
78
  run(["uv", "add", "--upgrade"] + pkgs, check=False)
70
79
 
71
80
  # Groups
@@ -74,27 +83,27 @@ def task_upgrade_deps():
74
83
  if g_deps:
75
84
  pkgs = [get_pkg_name(d) for d in g_deps if get_pkg_name(d)]
76
85
  if pkgs:
77
- print(f"🏗️ Upgrading group '{group}': {pkgs}")
86
+ safe_print(f"🏗️ Upgrading group '{group}': {pkgs}")
78
87
  run(
79
88
  ["uv", "add", "--group", group, "--upgrade"] + pkgs, check=False
80
89
  )
81
90
 
82
91
  # Lock file
83
- print("🔒 Refreshing lockfile...")
92
+ safe_print("🔒 Refreshing lockfile...")
84
93
  run(["uv", "lock", "--upgrade"], check=False)
85
94
 
86
95
  except Exception as e:
87
- print(f"⚠️ Dependency upgrade failed: {e}")
96
+ safe_print(f"⚠️ Dependency upgrade failed: {e}")
88
97
 
89
98
  if git_status_has_changes():
90
- print("✅ Dependencies updated.")
99
+ safe_print("✅ Dependencies updated.")
91
100
  run(["git", "add", "."])
92
101
  return True
93
102
  return False
94
103
 
95
104
 
96
105
  def task_version_bump():
97
- print("\n🏷️ --- 3. VERSION CHECK ---")
106
+ safe_print("\n🏷️ --- 3. VERSION CHECK ---")
98
107
  new_v = ""
99
108
 
100
109
  def get_ver(content):
@@ -118,7 +127,7 @@ def task_version_bump():
118
127
  return (0, 0, 0)
119
128
 
120
129
  try:
121
- print("🌐 Fetching remote to compare versions...")
130
+ safe_print("🌐 Fetching remote to compare versions...")
122
131
  run(["git", "fetch", "origin", "main"], check=False)
123
132
  try:
124
133
  base = subprocess.check_output(
@@ -128,18 +137,22 @@ def task_version_bump():
128
137
  base = "origin/main"
129
138
 
130
139
  # Read Main Version
131
- run(["git", "checkout", base, "--", str(VERSION_FILE)], capture=True)
132
- main_v = get_ver(VERSION_FILE.read_text())
140
+ try:
141
+ main_v_content = subprocess.check_output(
142
+ ["git", "show", f"{base}:{VERSION_FILE.as_posix()}"], text=True
143
+ )
144
+ main_v = get_ver(main_v_content)
145
+ except Exception:
146
+ main_v = None
133
147
 
134
- # Reset file and read Current Version
135
- run(["git", "checkout", "HEAD", "--", str(VERSION_FILE)], capture=True)
148
+ # Read Current Version
136
149
  curr_v = get_ver(VERSION_FILE.read_text())
137
150
 
138
- print(f"📊 Main: {main_v} | Current: {curr_v}")
151
+ safe_print(f"📊 Main: {main_v} | Current: {curr_v}")
139
152
 
140
153
  if main_v and curr_v and ver_tuple(curr_v) <= ver_tuple(main_v):
141
154
  new_v = bump(main_v)
142
- print(f"🚀 Bumping version to: {new_v}")
155
+ safe_print(f"🚀 Bumping version to: {new_v}")
143
156
  content = VERSION_FILE.read_text().replace(f'"{curr_v}"', f'"{new_v}"')
144
157
  VERSION_FILE.write_text(content)
145
158
 
@@ -147,7 +160,7 @@ def task_version_bump():
147
160
  return True, new_v
148
161
 
149
162
  except Exception as e:
150
- print(f"⚠️ Version check warning (non-fatal): {e}")
163
+ safe_print(f"⚠️ Version check warning (non-fatal): {e}")
151
164
 
152
165
  return False, new_v
153
166
 
@@ -166,7 +179,7 @@ def main():
166
179
 
167
180
  # --- CI Specific Logic ---
168
181
  if is_ci and (fixed_format or fixed_deps or fixed_version):
169
- print("\n📤 --- 4. PUSH & REPORT ---")
182
+ safe_print("\n📤 --- 4. PUSH & REPORT ---")
170
183
 
171
184
  run(["git", "config", "--global", "user.name", "github-actions[bot]"])
172
185
  run(
@@ -195,7 +208,7 @@ def main():
195
208
  try:
196
209
  run(["git", "commit", "-m", msg])
197
210
  target_ref = get_env("TARGET_REF")
198
- print(f"🔄 Rebase and pushing to {target_ref}...")
211
+ safe_print(f"🔄 Rebase and pushing to {target_ref}...")
199
212
  run(["git", "pull", "--rebase", "origin", target_ref], check=False)
200
213
  run(["git", "push", "origin", f"HEAD:{target_ref}"])
201
214
 
@@ -203,7 +216,7 @@ def main():
203
216
  with open(os.environ["GITHUB_OUTPUT"], "a") as f:
204
217
  f.write("changes_pushed=true\n")
205
218
  except subprocess.CalledProcessError as e:
206
- print(f"❌ ::error::Failed to push fixes. ({e})")
219
+ safe_print(f"❌ ::error::Failed to push fixes. ({e})")
207
220
  sys.exit(1)
208
221
 
209
222
  repo = get_env("GITHUB_REPO")
@@ -223,7 +236,7 @@ def main():
223
236
  pass
224
237
 
225
238
  if pr_num:
226
- print(f"💬 Posting status update to PR #{pr_num}...")
239
+ safe_print(f"💬 Posting status update to PR #{pr_num}...")
227
240
  fixes_list = ""
228
241
  if fixed_format:
229
242
  fixes_list += "- ✅ **Formatted Code**\n"
@@ -252,18 +265,18 @@ def main():
252
265
  check=False,
253
266
  )
254
267
 
255
- print(f"⚡ Triggering workflow: {workflow_name}...")
268
+ safe_print(f"⚡ Triggering workflow: {workflow_name}...")
256
269
  ret = run(
257
270
  ["gh", "workflow", "run", workflow_name, "--ref", target_ref], check=False
258
271
  )
259
272
 
260
273
  if ret.returncode != 0:
261
- print("⚠️ ::warning::Could not auto-trigger next run.")
274
+ safe_print("⚠️ ::warning::Could not auto-trigger next run.")
262
275
 
263
276
  sys.exit(0)
264
277
 
265
278
  else:
266
- print("\n✨ Clean run. No changes needed.")
279
+ safe_print("\n✨ Clean run. No changes needed.")
267
280
  if "GITHUB_OUTPUT" in os.environ:
268
281
  with open(os.environ["GITHUB_OUTPUT"], "a") as f:
269
282
  f.write("changes_pushed=false\n")
@@ -386,48 +386,43 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
386
386
  try:
387
387
  shared_state.set_state(shared_state_dict, shared_state_lock)
388
388
 
389
- shared_state.set_device_from_config()
389
+ while True:
390
+ shared_state.set_device_from_config()
390
391
 
391
- connection_established = (
392
- shared_state.get_device() and shared_state.get_device().name
393
- )
394
- if not connection_established:
395
- i = 0
396
- while i < 10:
397
- i += 1
392
+ device = shared_state.get_device()
393
+
394
+ try:
398
395
  info(
399
- f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"'
396
+ f'Connection to JDownloader successful. Device name: "{device.name}"'
400
397
  )
401
- time.sleep(60)
402
- shared_state.set_device_from_config()
403
- connection_established = (
404
- shared_state.get_device() and shared_state.get_device().name
405
- )
406
- if connection_established:
407
- break
398
+ except Exception as e:
399
+ info(f"Error connecting to JDownloader: {e}! Stopping Quasarr...")
400
+ sys.exit(1)
408
401
 
409
- try:
410
- info(
411
- f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"'
412
- )
413
- except Exception as e:
414
- info(f"Error connecting to JDownloader: {e}! Stopping Quasarr!")
415
- sys.exit(1)
402
+ try:
403
+ shared_state.set_device_settings()
404
+ except Exception as e:
405
+ print(f"Error checking settings: {e}")
416
406
 
417
- try:
418
- shared_state.set_device_settings()
419
- except Exception as e:
420
- print(f"Error checking settings: {e}")
407
+ try:
408
+ shared_state.update_jdownloader()
409
+ except Exception as e:
410
+ print(f"Error updating JDownloader: {e}")
421
411
 
422
- try:
423
- shared_state.update_jdownloader()
424
- except Exception as e:
425
- print(f"Error updating JDownloader: {e}")
412
+ try:
413
+ shared_state.start_downloads()
414
+ except Exception as e:
415
+ print(f"Error starting downloads: {e}")
426
416
 
427
- try:
428
- shared_state.start_downloads()
429
- except Exception as e:
430
- print(f"Error starting downloads: {e}")
417
+ while True:
418
+ time.sleep(300)
419
+ device_state = shared_state.check_device(
420
+ shared_state.values.get("device")
421
+ )
422
+ if not device_state:
423
+ info("Lost connection to JDownloader. Reconnecting...")
424
+ shared_state.update("device", False)
425
+ break
431
426
 
432
427
  except KeyboardInterrupt:
433
428
  pass
@@ -8,6 +8,7 @@ import quasarr.providers.html_images as images
8
8
  from quasarr.api.arr import setup_arr_routes
9
9
  from quasarr.api.captcha import setup_captcha_routes
10
10
  from quasarr.api.config import setup_config
11
+ from quasarr.api.jdownloader import get_jdownloader_modal_script, get_jdownloader_status
11
12
  from quasarr.api.packages import setup_packages_routes
12
13
  from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
13
14
  from quasarr.api.statistics import setup_statistics
@@ -49,12 +50,9 @@ def get_api(shared_state_dict, shared_state_lock):
49
50
  protected = shared_state.get_db("protected").retrieve_all_titles()
50
51
  api_key = Config("API").get("key")
51
52
 
52
- # Get quick status summary
53
- try:
54
- device = shared_state.values.get("device")
55
- jd_connected = device is not None and device is not False
56
- except:
57
- jd_connected = False
53
+ # Get JDownloader status and modal script
54
+ jd_status = get_jdownloader_status(shared_state)
55
+ jd_modal_script = get_jdownloader_modal_script()
58
56
 
59
57
  # Calculate hostname status
60
58
  hostnames_config = Config("Hostnames")
@@ -126,10 +124,14 @@ def get_api(shared_state_dict, shared_state_lock):
126
124
  # Status bars
127
125
  status_bars = f"""
128
126
  <div class="status-bar">
129
- <span class="status-pill {"success" if jd_connected else "error"}">
130
- {"" if jd_connected else "❌"} JDownloader {"connected" if jd_connected else "disconnected"}
127
+ <span class="status-pill {jd_status['status_class']}"
128
+ onclick="openJDownloaderModal()"
129
+ title="Click to configure JDownloader">
130
+ {jd_status['status_text']}
131
131
  </span>
132
- <span class="status-pill {hostname_status_class}">
132
+ <span class="status-pill {hostname_status_class}"
133
+ onclick="location.href='/hostnames'"
134
+ title="Click to configure Hostnames">
133
135
  {hostname_status_emoji} {hostname_status_text}
134
136
  </span>
135
137
  </div>
@@ -209,6 +211,11 @@ def get_api(shared_state_dict, shared_state_lock):
209
211
  padding: 8px 16px;
210
212
  border-radius: 0.5rem;
211
213
  font-weight: 500;
214
+ transition: transform 0.1s ease;
215
+ cursor: pointer;
216
+ }}
217
+ .status-pill:hover {{
218
+ transform: scale(1.05);
212
219
  }}
213
220
  .status-pill.success {{
214
221
  background: var(--status-success-bg, #e8f5e9);
@@ -367,15 +374,15 @@ def get_api(shared_state_dict, shared_state_lock):
367
374
  /* Dark mode */
368
375
  @media (prefers-color-scheme: dark) {{
369
376
  :root {{
370
- --status-success-bg: #1b5e20;
371
- --status-success-color: #a5d6a7;
372
- --status-success-border: #2e7d32;
377
+ --status-success-bg: #1c4532;
378
+ --status-success-color: #68d391;
379
+ --status-success-border: #276749;
373
380
  --status-warning-bg: #3d3520;
374
381
  --status-warning-color: #ffb74d;
375
382
  --status-warning-border: #d69e2e;
376
- --status-error-bg: #b71c1c;
377
- --status-error-color: #ef9a9a;
378
- --status-error-border: #c62828;
383
+ --status-error-bg: #3d2d2d;
384
+ --status-error-color: #fc8181;
385
+ --status-error-border: #c53030;
379
386
  --alert-warning-bg: #3d3520;
380
387
  --alert-warning-border: #d69e2e;
381
388
  --card-bg: #2d3748;
@@ -478,6 +485,7 @@ def get_api(shared_state_dict, shared_state_lock):
478
485
  );
479
486
  }}
480
487
  </script>
488
+ {jd_modal_script}
481
489
  """
482
490
  # Add logout link for form auth
483
491
  logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ""
@@ -56,6 +56,16 @@ def parse_payload(payload_str):
56
56
 
57
57
 
58
58
  def setup_arr_routes(app):
59
+ def check_user_agent():
60
+ user_agent = request.headers.get("User-Agent") or ""
61
+ if not any(
62
+ tool in user_agent.lower() for tool in ["radarr", "sonarr", "lazylibrarian"]
63
+ ):
64
+ msg = f"Unsupported User-Agent: {user_agent}. Quasarr as a compatibility layer must be called by Radarr, Sonarr or LazyLibrarian directly."
65
+ info(msg)
66
+ abort(406, msg)
67
+ return user_agent
68
+
59
69
  @app.get("/download/")
60
70
  def fake_nzb_file():
61
71
  payload = request.query.payload
@@ -75,6 +85,7 @@ def setup_arr_routes(app):
75
85
  @app.post("/api")
76
86
  @require_api_key
77
87
  def download_fake_nzb_file():
88
+ request_from = check_user_agent()
78
89
  downloads = request.files.getall("name")
79
90
  nzo_ids = [] # naming structure for package IDs expected in newznab
80
91
 
@@ -97,7 +108,6 @@ def setup_arr_routes(app):
97
108
  source_key = root.find(".//file").attrib.get("source_key") or None
98
109
 
99
110
  info(f'Attempting download for "{title}"')
100
- request_from = request.headers.get("User-Agent")
101
111
  downloaded = download(
102
112
  shared_state,
103
113
  request_from,
@@ -128,6 +138,8 @@ def setup_arr_routes(app):
128
138
  @app.get("/api/<mirror>")
129
139
  @require_api_key
130
140
  def quasarr_api(mirror=None):
141
+ request_from = check_user_agent()
142
+
131
143
  api_type = (
132
144
  "arr_download_client"
133
145
  if request.query.mode
@@ -198,7 +210,6 @@ def setup_arr_routes(app):
198
210
 
199
211
  nzo_ids = []
200
212
  info(f'Attempting download for "{parsed_payload["title"]}"')
201
- request_from = "lazylibrarian"
202
213
 
203
214
  downloaded = download(
204
215
  shared_state,
@@ -267,8 +278,6 @@ def setup_arr_routes(app):
267
278
  )
268
279
 
269
280
  mode = request.query.t
270
- request_from = request.headers.get("User-Agent")
271
-
272
281
  if mode == "caps":
273
282
  info(f"Providing indexer capability information to {request_from}")
274
283
  return """<?xml version="1.0" encoding="UTF-8"?>
@@ -352,10 +361,10 @@ def setup_arr_routes(app):
352
361
  mirror=mirror,
353
362
  )
354
363
  else:
355
- info(
364
+ # sonarr expects this but we will not support non-imdbid searches
365
+ debug(
356
366
  f"Ignoring search request from {request_from} - only imdbid searches are supported"
357
367
  )
358
- releases = [] # sonarr expects this but we will not support non-imdbid searches
359
368
 
360
369
  items = ""
361
370
  for release in releases:
@@ -11,6 +11,7 @@ import requests
11
11
  from bottle import HTTPResponse, redirect, request, response
12
12
 
13
13
  import quasarr.providers.html_images as images
14
+ from quasarr.api.jdownloader import get_jdownloader_disconnected_page
14
15
  from quasarr.downloads.linkcrypters.filecrypt import DLC, get_filecrypt_links
15
16
  from quasarr.downloads.packages import delete_package
16
17
  from quasarr.providers import obfuscated, shared_state
@@ -46,15 +47,7 @@ def setup_captcha_routes(app):
46
47
  except KeyError:
47
48
  device = None
48
49
  if not device:
49
- return render_centered_html(f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
50
- <div class="status-bar">
51
- <span class="status-pill error">
52
- ❌ JDownloader disconnected
53
- </span>
54
- </div>
55
- <p>
56
- {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
57
- </p>''')
50
+ return get_jdownloader_disconnected_page(shared_state)
58
51
 
59
52
  protected = shared_state.get_db("protected").retrieve_all_titles()
60
53
  if not protected: