quasarr 2.4.11__tar.gz → 2.5.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 (96) hide show
  1. {quasarr-2.4.11 → quasarr-2.5.0}/.github/workflows/PullRequests.yml +12 -5
  2. {quasarr-2.4.11 → quasarr-2.5.0}/PKG-INFO +1 -1
  3. {quasarr-2.4.11 → quasarr-2.5.0}/pre-commit.py +40 -27
  4. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/__init__.py +6 -6
  5. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/config/__init__.py +22 -169
  6. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/version.py +1 -1
  7. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/__init__.py +86 -15
  8. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/storage/setup.py +517 -236
  9. {quasarr-2.4.11 → quasarr-2.5.0}/.github/FUNDING.yml +0 -0
  10. {quasarr-2.4.11 → quasarr-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  11. {quasarr-2.4.11 → quasarr-2.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  12. {quasarr-2.4.11 → quasarr-2.5.0}/.github/workflows/HostnameRedaction.yml +0 -0
  13. {quasarr-2.4.11 → quasarr-2.5.0}/.github/workflows/Release.yml +0 -0
  14. {quasarr-2.4.11 → quasarr-2.5.0}/.gitignore +0 -0
  15. {quasarr-2.4.11 → quasarr-2.5.0}/.pre-commit-config.yaml +0 -0
  16. {quasarr-2.4.11 → quasarr-2.5.0}/CONTRIBUTING.md +0 -0
  17. {quasarr-2.4.11 → quasarr-2.5.0}/LICENSE +0 -0
  18. {quasarr-2.4.11 → quasarr-2.5.0}/Quasarr.png +0 -0
  19. {quasarr-2.4.11 → quasarr-2.5.0}/Quasarr.py +0 -0
  20. {quasarr-2.4.11 → quasarr-2.5.0}/README.md +0 -0
  21. {quasarr-2.4.11 → quasarr-2.5.0}/docker/Dockerfile +0 -0
  22. {quasarr-2.4.11 → quasarr-2.5.0}/docker/dev-services-compose.yml +0 -0
  23. {quasarr-2.4.11 → quasarr-2.5.0}/docker/docker-compose.yml +0 -0
  24. {quasarr-2.4.11 → quasarr-2.5.0}/pyproject.toml +0 -0
  25. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/__init__.py +0 -0
  26. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/arr/__init__.py +0 -0
  27. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/captcha/__init__.py +0 -0
  28. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/packages/__init__.py +0 -0
  29. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/sponsors_helper/__init__.py +0 -0
  30. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/api/statistics/__init__.py +0 -0
  31. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/__init__.py +0 -0
  32. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/linkcrypters/__init__.py +0 -0
  33. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/linkcrypters/al.py +0 -0
  34. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
  35. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/linkcrypters/hide.py +0 -0
  36. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/packages/__init__.py +0 -0
  37. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/__init__.py +0 -0
  38. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/al.py +0 -0
  39. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/by.py +0 -0
  40. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/dd.py +0 -0
  41. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/dj.py +0 -0
  42. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/dl.py +0 -0
  43. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/dt.py +0 -0
  44. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/dw.py +0 -0
  45. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/he.py +0 -0
  46. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/mb.py +0 -0
  47. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/nk.py +0 -0
  48. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/nx.py +0 -0
  49. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/sf.py +0 -0
  50. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/sj.py +0 -0
  51. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/sl.py +0 -0
  52. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/wd.py +0 -0
  53. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/downloads/sources/wx.py +0 -0
  54. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/__init__.py +0 -0
  55. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/auth.py +0 -0
  56. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/cloudflare.py +0 -0
  57. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/hostname_issues.py +0 -0
  58. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/html_images.py +0 -0
  59. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/html_templates.py +0 -0
  60. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/imdb_metadata.py +0 -0
  61. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/jd_cache.py +0 -0
  62. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/log.py +0 -0
  63. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/myjd_api.py +0 -0
  64. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/notifications.py +0 -0
  65. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/obfuscated.py +0 -0
  66. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/sessions/__init__.py +0 -0
  67. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/sessions/al.py +0 -0
  68. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/sessions/dd.py +0 -0
  69. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/sessions/dl.py +0 -0
  70. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/sessions/nx.py +0 -0
  71. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/shared_state.py +0 -0
  72. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/statistics.py +0 -0
  73. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/utils.py +0 -0
  74. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/providers/web_server.py +0 -0
  75. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/__init__.py +0 -0
  76. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/al.py +0 -0
  77. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/by.py +0 -0
  78. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/dd.py +0 -0
  79. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/dj.py +0 -0
  80. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/dl.py +0 -0
  81. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/dt.py +0 -0
  82. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/dw.py +0 -0
  83. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/fx.py +0 -0
  84. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/he.py +0 -0
  85. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/mb.py +0 -0
  86. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/nk.py +0 -0
  87. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/nx.py +0 -0
  88. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/sf.py +0 -0
  89. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/sj.py +0 -0
  90. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/sl.py +0 -0
  91. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/wd.py +0 -0
  92. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/search/sources/wx.py +0 -0
  93. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/storage/__init__.py +0 -0
  94. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/storage/config.py +0 -0
  95. {quasarr-2.4.11 → quasarr-2.5.0}/quasarr/storage/sqlite_database.py +0 -0
  96. {quasarr-2.4.11 → quasarr-2.5.0}/uv.lock +0 -0
@@ -57,6 +57,7 @@ jobs:
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,8 +69,10 @@ 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 ]
@@ -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,7 +187,7 @@ 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 }}
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quasarr
3
- Version: 2.4.11
3
+ Version: 2.5.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")
@@ -367,15 +367,15 @@ def get_api(shared_state_dict, shared_state_lock):
367
367
  /* Dark mode */
368
368
  @media (prefers-color-scheme: dark) {{
369
369
  :root {{
370
- --status-success-bg: #1b5e20;
371
- --status-success-color: #a5d6a7;
372
- --status-success-border: #2e7d32;
370
+ --status-success-bg: #1c4532;
371
+ --status-success-color: #68d391;
372
+ --status-success-border: #276749;
373
373
  --status-warning-bg: #3d3520;
374
374
  --status-warning-color: #ffb74d;
375
375
  --status-warning-border: #d69e2e;
376
- --status-error-bg: #b71c1c;
377
- --status-error-color: #ef9a9a;
378
- --status-error-border: #c62828;
376
+ --status-error-bg: #3d2d2d;
377
+ --status-error-color: #fc8181;
378
+ --status-error-border: #c53030;
379
379
  --alert-warning-bg: #3d3520;
380
380
  --alert-warning-border: #d69e2e;
381
381
  --card-bg: #2d3748;
@@ -3,27 +3,24 @@
3
3
  # Project by https://github.com/rix1337
4
4
 
5
5
  import os
6
- import re
7
6
  import signal
8
7
  import threading
9
8
  import time
10
- from urllib.parse import urlparse
11
9
 
12
- import requests
13
- from bottle import request, response
10
+ from bottle import response
14
11
 
15
- from quasarr.providers.html_templates import render_button, render_fail, render_form
12
+ from quasarr.providers.html_templates import render_button, render_form
16
13
  from quasarr.providers.log import info
17
- from quasarr.providers.shared_state import extract_valid_hostname
18
- from quasarr.providers.utils import (
19
- check_flaresolverr,
20
- extract_allowed_keys,
21
- extract_kv_pairs,
22
- )
23
14
  from quasarr.storage.config import Config
24
15
  from quasarr.storage.setup import (
16
+ check_credentials,
17
+ clear_skip_login,
18
+ delete_skip_flaresolverr_preference,
19
+ get_flaresolverr_status_data,
20
+ get_skip_login,
25
21
  hostname_form_html,
26
- render_reconnect_success,
22
+ import_hostnames_from_url,
23
+ save_flaresolverr_url,
27
24
  save_hostnames,
28
25
  )
29
26
  from quasarr.storage.sqlite_database import DataBase
@@ -50,7 +47,6 @@ def setup_config(app, shared_state):
50
47
  hostname_form_html(
51
48
  shared_state,
52
49
  message,
53
- show_restart_button=True,
54
50
  show_skip_management=True,
55
51
  )
56
52
  + back_button,
@@ -60,97 +56,21 @@ def setup_config(app, shared_state):
60
56
  def hostnames_api():
61
57
  return save_hostnames(shared_state, timeout=1, first_run=False)
62
58
 
63
- @app.post("/api/hostnames/import-url")
64
- def import_hostnames_from_url():
65
- """Fetch URL and parse hostnames, return JSON for JS to populate fields."""
66
- response.content_type = "application/json"
67
- try:
68
- data = request.json
69
- url = data.get("url", "").strip()
70
-
71
- if not url:
72
- return {"success": False, "error": "No URL provided"}
73
-
74
- # Validate URL
75
- parsed = urlparse(url)
76
- if parsed.scheme not in ("http", "https") or not parsed.netloc:
77
- return {"success": False, "error": "Invalid URL format"}
78
-
79
- # Fetch content
80
- try:
81
- resp = requests.get(url, timeout=15)
82
- resp.raise_for_status()
83
- content = resp.text
84
- except requests.RequestException as e:
85
- info(f"Failed to fetch hostnames URL: {e}")
86
- return {
87
- "success": False,
88
- "error": "Failed to fetch URL. Check the console log for details.",
89
- }
90
-
91
- # Parse hostnames
92
- allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
93
- results = extract_kv_pairs(content, allowed_keys)
94
-
95
- if not results:
96
- return {
97
- "success": False,
98
- "error": "No hostnames found in the provided URL",
99
- }
59
+ @app.post("/api/hostnames/check-credentials/<shorthand>")
60
+ def check_credentials_api(shorthand):
61
+ return check_credentials(shared_state, shorthand)
100
62
 
101
- # Validate each hostname
102
- valid_hostnames = {}
103
- invalid_hostnames = {}
104
- for shorthand, hostname in results.items():
105
- domain_check = extract_valid_hostname(hostname, shorthand)
106
- domain = domain_check.get("domain")
107
- if domain:
108
- valid_hostnames[shorthand] = domain
109
- else:
110
- invalid_hostnames[shorthand] = domain_check.get(
111
- "message", "Invalid"
112
- )
113
-
114
- if not valid_hostnames:
115
- return {
116
- "success": False,
117
- "error": "No valid hostnames found in the provided URL",
118
- }
119
-
120
- return {
121
- "success": True,
122
- "hostnames": valid_hostnames,
123
- "errors": invalid_hostnames,
124
- }
125
-
126
- except Exception as e:
127
- return {"success": False, "error": f"Error: {str(e)}"}
63
+ @app.post("/api/hostnames/import-url")
64
+ def import_hostnames_route():
65
+ return import_hostnames_from_url()
128
66
 
129
67
  @app.get("/api/skip-login")
130
- def get_skip_login():
131
- """Return list of hostnames with skipped login."""
132
- response.content_type = "application/json"
133
- skip_db = DataBase("skip_login")
134
- login_required_sites = ["al", "dd", "dl", "nx"]
135
- skipped = []
136
- for site in login_required_sites:
137
- if skip_db.retrieve(site):
138
- skipped.append(site)
139
- return {"skipped": skipped}
68
+ def get_skip_login_route():
69
+ return get_skip_login()
140
70
 
141
71
  @app.delete("/api/skip-login/<shorthand>")
142
- def clear_skip_login(shorthand):
143
- """Clear skip login preference for a hostname."""
144
- response.content_type = "application/json"
145
- shorthand = shorthand.lower()
146
- login_required_sites = ["al", "dd", "dl", "nx"]
147
- if shorthand not in login_required_sites:
148
- return {"success": False, "error": f"Invalid shorthand: {shorthand}"}
149
-
150
- skip_db = DataBase("skip_login")
151
- skip_db.delete(shorthand)
152
- info(f'Skip login preference cleared for "{shorthand.upper()}"')
153
- return {"success": True}
72
+ def clear_skip_login_route(shorthand):
73
+ return clear_skip_login(shorthand)
154
74
 
155
75
  @app.get("/flaresolverr")
156
76
  def flaresolverr_ui():
@@ -183,12 +103,6 @@ def setup_config(app, shared_state):
183
103
  {form_content}
184
104
  {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
185
105
  </form>
186
- <p style="font-size:0.875rem; color:var(--secondary, #6c757d); margin-top:1rem;">
187
- A restart is recommended after configuring FlareSolverr.
188
- </p>
189
- <div class="section-divider" style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--divider-color, #dee2e6);">
190
- {render_button("Restart Quasarr", "secondary", {"type": "button", "onclick": "confirmRestart()"})}
191
- </div>
192
106
  <p>{render_button("Back", "secondary", {"onclick": "location.href='/';"})}</p>
193
107
  <script>
194
108
  var formSubmitted = false;
@@ -278,78 +192,17 @@ def setup_config(app, shared_state):
278
192
  @app.post("/api/flaresolverr")
279
193
  def set_flaresolverr_url():
280
194
  """Save FlareSolverr URL from web UI."""
281
- url = request.forms.get("url", "").strip()
282
- config = Config("FlareSolverr")
283
-
284
- if not url:
285
- return render_fail("Please provide a FlareSolverr URL.")
286
-
287
- if not url.startswith("http://") and not url.startswith("https://"):
288
- url = "http://" + url
289
-
290
- # Validate URL format
291
- if not re.search(r"/v\d+$", url):
292
- return render_fail(
293
- "FlareSolverr URL must end with /v1 (or similar version path)."
294
- )
295
-
296
- try:
297
- headers = {"Content-Type": "application/json"}
298
- data = {
299
- "cmd": "request.get",
300
- "url": "http://www.google.com/",
301
- "maxTimeout": 30000,
302
- }
303
- resp = requests.post(url, headers=headers, json=data, timeout=30)
304
- if resp.status_code == 200:
305
- json_data = resp.json()
306
- if json_data.get("status") == "ok":
307
- config.save("url", url)
308
- # Clear skip preference since we now have a working URL
309
- DataBase("skip_flaresolverr").delete("skipped")
310
- # Update user agent from FlareSolverr response
311
- solution = json_data.get("solution", {})
312
- solution_ua = solution.get("userAgent")
313
- if solution_ua:
314
- shared_state.update("user_agent", solution_ua)
315
- info(f'FlareSolverr URL configured: "{url}"')
316
- return render_reconnect_success(
317
- "FlareSolverr URL saved successfully! A restart is recommended."
318
- )
319
- else:
320
- return render_fail(
321
- f"FlareSolverr returned unexpected status: {json_data.get('status')}"
322
- )
323
- except requests.RequestException:
324
- return render_fail("Could not reach FlareSolverr!")
325
-
326
- return render_fail(
327
- "Could not reach FlareSolverr at that URL (expected HTTP 200)."
328
- )
195
+ return save_flaresolverr_url(shared_state)
329
196
 
330
197
  @app.get("/api/flaresolverr/status")
331
198
  def get_flaresolverr_status():
332
199
  """Return FlareSolverr configuration status."""
333
- response.content_type = "application/json"
334
- skip_db = DataBase("skip_flaresolverr")
335
- is_skipped = bool(skip_db.retrieve("skipped"))
336
- current_url = Config("FlareSolverr").get("url") or ""
337
-
338
- # Test connection if URL is set
339
- is_working = False
340
- if current_url and not is_skipped:
341
- is_working = check_flaresolverr(shared_state, current_url)
342
-
343
- return {"skipped": is_skipped, "url": current_url, "working": is_working}
200
+ return get_flaresolverr_status_data(shared_state)
344
201
 
345
202
  @app.delete("/api/skip-flaresolverr")
346
203
  def clear_skip_flaresolverr():
347
204
  """Clear skip FlareSolverr preference."""
348
- response.content_type = "application/json"
349
- skip_db = DataBase("skip_flaresolverr")
350
- skip_db.delete("skipped")
351
- info("Skip FlareSolverr preference cleared")
352
- return {"success": True}
205
+ return delete_skip_flaresolverr_preference()
353
206
 
354
207
  @app.post("/api/restart")
355
208
  def restart_quasarr():
@@ -5,7 +5,7 @@
5
5
  import re
6
6
  import sys
7
7
 
8
- __version__ = "2.4.11"
8
+ __version__ = "2.5.0"
9
9
 
10
10
 
11
11
  def get_version():