quasarr 2.4.7__tar.gz → 2.4.9__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.
- {quasarr-2.4.7 → quasarr-2.4.9}/.github/workflows/HostnameRedaction.yml +1 -1
- quasarr-2.4.9/.github/workflows/PullRequests.yml +434 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/.github/workflows/Release.yml +59 -8
- {quasarr-2.4.7 → quasarr-2.4.9}/.gitignore +1 -2
- {quasarr-2.4.7 → quasarr-2.4.9}/PKG-INFO +2 -2
- {quasarr-2.4.7 → quasarr-2.4.9}/Quasarr.py +1 -1
- {quasarr-2.4.7 → quasarr-2.4.9}/docker/Dockerfile +5 -4
- quasarr-2.4.9/docker/dev-setup.md +55 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/pyproject.toml +13 -3
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/__init__.py +134 -70
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/__init__.py +40 -31
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/arr/__init__.py +116 -108
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/captcha/__init__.py +262 -137
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/config/__init__.py +76 -46
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/packages/__init__.py +138 -102
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/sponsors_helper/__init__.py +29 -16
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/api/statistics/__init__.py +19 -19
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/__init__.py +165 -72
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/linkcrypters/al.py +35 -18
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/linkcrypters/filecrypt.py +107 -52
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/linkcrypters/hide.py +5 -6
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/packages/__init__.py +342 -177
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/al.py +191 -100
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/by.py +31 -13
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/dd.py +27 -14
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/dj.py +1 -3
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/dl.py +126 -71
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/dt.py +11 -5
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/dw.py +28 -14
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/he.py +32 -24
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/mb.py +19 -9
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/nk.py +14 -10
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/nx.py +8 -18
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/sf.py +45 -20
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/sj.py +1 -3
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/sl.py +9 -5
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/wd.py +32 -12
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/wx.py +35 -21
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/auth.py +42 -37
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/cloudflare.py +28 -30
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/hostname_issues.py +2 -1
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/html_images.py +2 -2
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/html_templates.py +22 -14
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/imdb_metadata.py +149 -80
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/jd_cache.py +131 -39
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/log.py +1 -1
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/myjd_api.py +260 -196
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/notifications.py +53 -41
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/obfuscated.py +9 -4
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/sessions/al.py +71 -55
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/sessions/dd.py +21 -14
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/sessions/dl.py +30 -19
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/sessions/nx.py +23 -14
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/shared_state.py +292 -141
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/statistics.py +75 -43
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/utils.py +33 -27
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/version.py +45 -14
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/web_server.py +10 -5
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/__init__.py +30 -18
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/al.py +124 -73
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/by.py +110 -59
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/dd.py +57 -35
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/dj.py +69 -48
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/dl.py +159 -100
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/dt.py +110 -74
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/dw.py +121 -61
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/fx.py +108 -62
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/he.py +78 -49
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/mb.py +96 -48
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/nk.py +80 -50
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/nx.py +91 -62
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/sf.py +171 -106
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/sj.py +69 -48
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/sl.py +115 -71
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/wd.py +67 -44
- quasarr-2.4.9/quasarr/search/sources/wx.py +417 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/storage/config.py +65 -52
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/storage/setup.py +238 -140
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/storage/sqlite_database.py +10 -4
- quasarr-2.4.9/uv.lock +431 -0
- quasarr-2.4.7/.github/Changelog.md +0 -5
- quasarr-2.4.7/.github/workflows/Beta.yml +0 -197
- quasarr-2.4.7/.github/workflows/PrVersionBumpCheck.yml +0 -138
- quasarr-2.4.7/docker/dev-setup.md +0 -21
- quasarr-2.4.7/quasarr/search/sources/wx.py +0 -352
- {quasarr-2.4.7 → quasarr-2.4.9}/.github/FUNDING.yml +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/LICENSE +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/Quasarr.png +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/README.md +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/docker/dev-services-compose.yml +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/docker/docker-compose.yml +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/linkcrypters/__init__.py +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/downloads/sources/__init__.py +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/__init__.py +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/providers/sessions/__init__.py +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/search/sources/__init__.py +0 -0
- {quasarr-2.4.7 → quasarr-2.4.9}/quasarr/storage/__init__.py +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
name: CI/CD & Beta Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
GHCR_ENDPOINT: "ghcr.io/rix1337/quasarr"
|
|
13
|
+
DESCRIPTION: "Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian."
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
quality-check:
|
|
17
|
+
name: Check & Auto-Fix
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write # To push commits
|
|
21
|
+
pull-requests: write # To comment on PRs
|
|
22
|
+
actions: write # To trigger the next run
|
|
23
|
+
outputs:
|
|
24
|
+
changes_pushed: ${{ steps.manager.outputs.changes_pushed }}
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
|
+
with:
|
|
28
|
+
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
|
29
|
+
ref: ${{ github.head_ref || github.ref_name }}
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- name: Install uv
|
|
33
|
+
uses: astral-sh/setup-uv@v5
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
- run: uv python install 3.12
|
|
37
|
+
|
|
38
|
+
- name: Fix, Bump, Report & Re-Trigger
|
|
39
|
+
id: manager
|
|
40
|
+
env:
|
|
41
|
+
GH_TOKEN: ${{ github.token }}
|
|
42
|
+
PR_NUMBER_ENV: ${{ github.event.pull_request.number }}
|
|
43
|
+
TARGET_REF: ${{ github.head_ref || github.ref_name }}
|
|
44
|
+
GITHUB_REPO: ${{ github.repository }}
|
|
45
|
+
WORKFLOW_NAME: ${{ github.workflow }}
|
|
46
|
+
run: |
|
|
47
|
+
git config --global user.name "github-actions[bot]"
|
|
48
|
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
49
|
+
|
|
50
|
+
uv run python - <<'EOF'
|
|
51
|
+
import os, re, subprocess, sys, json
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
# --- CONFIG ---
|
|
55
|
+
VERSION_FILE = Path("quasarr/providers/version.py")
|
|
56
|
+
TARGET_REF = os.environ["TARGET_REF"]
|
|
57
|
+
REPO = os.environ["GITHUB_REPO"]
|
|
58
|
+
WORKFLOW_NAME = os.environ["WORKFLOW_NAME"]
|
|
59
|
+
fixed_format = False
|
|
60
|
+
fixed_version = False
|
|
61
|
+
|
|
62
|
+
# --- 1. FORMATTING ---
|
|
63
|
+
print("Running Ruff...")
|
|
64
|
+
subprocess.run(["uv", "run", "ruff", "check", "--select", "I", "--fix", "."], check=False)
|
|
65
|
+
subprocess.run(["uv", "run", "ruff", "format", "."], check=False)
|
|
66
|
+
|
|
67
|
+
if subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True).stdout.strip():
|
|
68
|
+
fixed_format = True
|
|
69
|
+
|
|
70
|
+
# --- 2. VERSION BUMP ---
|
|
71
|
+
print("Checking Version...")
|
|
72
|
+
def get_ver(c):
|
|
73
|
+
m = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', c)
|
|
74
|
+
return m.group(1) if m else None
|
|
75
|
+
|
|
76
|
+
def bump(v):
|
|
77
|
+
p = v.split('.')
|
|
78
|
+
while len(p)<3: p.append('0')
|
|
79
|
+
try: p[-1] = str(int(p[-1])+1)
|
|
80
|
+
except: p.append('1')
|
|
81
|
+
return ".".join(p)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
subprocess.run(["git", "fetch", "origin", "main"], check=True, capture_output=True)
|
|
85
|
+
try: base = subprocess.check_output(["git", "merge-base", "HEAD", "origin/main"], text=True).strip()
|
|
86
|
+
except: base = "origin/main"
|
|
87
|
+
|
|
88
|
+
subprocess.run(["git", "checkout", base, "--", str(VERSION_FILE)], check=True, capture_output=True)
|
|
89
|
+
main_v = get_ver(VERSION_FILE.read_text())
|
|
90
|
+
subprocess.run(["git", "checkout", "HEAD", "--", str(VERSION_FILE)], check=True, capture_output=True)
|
|
91
|
+
curr_v = get_ver(VERSION_FILE.read_text())
|
|
92
|
+
|
|
93
|
+
print(f"Main: {main_v} | Current: {curr_v}")
|
|
94
|
+
if curr_v == main_v:
|
|
95
|
+
new_v = bump(curr_v)
|
|
96
|
+
print(f">> Bumping to {new_v}")
|
|
97
|
+
VERSION_FILE.write_text(VERSION_FILE.read_text().replace(f'"{curr_v}"', f'"{new_v}"'))
|
|
98
|
+
fixed_version = True
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Version check warning (non-fatal): {e}")
|
|
101
|
+
|
|
102
|
+
# --- 3. PUSH & REPORT ---
|
|
103
|
+
if fixed_format or fixed_version:
|
|
104
|
+
try:
|
|
105
|
+
print(">> Committing fixes...")
|
|
106
|
+
subprocess.run(["git", "add", "."], check=True)
|
|
107
|
+
msg = "chore: auto-fix " + ("fmt & ver" if fixed_format and fixed_version else "fmt" if fixed_format else "ver")
|
|
108
|
+
subprocess.run(["git", "commit", "-m", msg], check=True)
|
|
109
|
+
|
|
110
|
+
# Pull --rebase to avoid race conditions (crashes) if remote changed
|
|
111
|
+
subprocess.run(["git", "pull", "--rebase", "origin", TARGET_REF], check=False)
|
|
112
|
+
subprocess.run(["git", "push", "origin", f"HEAD:{TARGET_REF}"], check=True)
|
|
113
|
+
|
|
114
|
+
with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write("changes_pushed=true\n")
|
|
115
|
+
except subprocess.CalledProcessError as e:
|
|
116
|
+
print(f"::error::Failed to push fixes. Please pull the latest changes and try again. ({e})")
|
|
117
|
+
# Exit 1 so the job shows red, but cleanly.
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
# --- FIND PR ---
|
|
121
|
+
pr_num = os.environ.get("PR_NUMBER_ENV")
|
|
122
|
+
if not pr_num:
|
|
123
|
+
try:
|
|
124
|
+
pr_json = subprocess.check_output(["gh", "pr", "list", "--head", TARGET_REF, "--json", "number"], text=True)
|
|
125
|
+
prs = json.loads(pr_json)
|
|
126
|
+
if prs: pr_num = str(prs[0]['number'])
|
|
127
|
+
except: pass
|
|
128
|
+
|
|
129
|
+
# --- POST "AUTO-FIX" COMMENT (Original Formatting) ---
|
|
130
|
+
if pr_num:
|
|
131
|
+
fixes_list = ""
|
|
132
|
+
if fixed_format: fixes_list += "- ✅ **Formatted Code** (Imports & Layout)\n"
|
|
133
|
+
if fixed_version: fixes_list += "- ✅ **Bumped Version**\n"
|
|
134
|
+
|
|
135
|
+
body = "### 🤖 Auto-Fix Applied\n"
|
|
136
|
+
body += "I fixed the following issues so we can merge:\n"
|
|
137
|
+
body += fixes_list + "\n"
|
|
138
|
+
body += "**Note:** The build is now **GREEN** 🟢, but your local branch is out of sync.\n"
|
|
139
|
+
body += f"Please run this locally:\n```bash\ngit pull origin {TARGET_REF}\n```\n"
|
|
140
|
+
body += "\n---\n#### 💡 For future reference:\n\n"
|
|
141
|
+
if fixed_format:
|
|
142
|
+
body += "**To check formatting before pushing:**\n```bash\nuv run ruff check --select I --fix .\nuv run ruff format .\n```\n\n"
|
|
143
|
+
if fixed_version:
|
|
144
|
+
body += "**To update the version manually:**\nEdit the `__version__` string in:\n`quasarr/providers/version.py`\n"
|
|
145
|
+
|
|
146
|
+
Path("comment.md").write_text(body, encoding="utf-8")
|
|
147
|
+
subprocess.run(["gh", "pr", "comment", pr_num, "--body-file", "comment.md"], check=False)
|
|
148
|
+
|
|
149
|
+
# --- POST "RE-TRIGGER" COMMENT ---
|
|
150
|
+
print(f">> Triggering new workflow run for: {WORKFLOW_NAME}...")
|
|
151
|
+
|
|
152
|
+
if pr_num:
|
|
153
|
+
actions_url = f"https://github.com/{REPO}/actions?query=branch%3A{TARGET_REF}"
|
|
154
|
+
retrigger_body = "🚀 **Beta Build Triggered!**\n\n"
|
|
155
|
+
retrigger_body += "I have automatically started a new workflow run on the updated branch.\n"
|
|
156
|
+
retrigger_body += f"\n[**👉 Click here to view the new run**]({actions_url})"
|
|
157
|
+
|
|
158
|
+
Path("retrigger.md").write_text(retrigger_body, encoding="utf-8")
|
|
159
|
+
subprocess.run(["gh", "pr", "comment", pr_num, "--body-file", "retrigger.md"], check=False)
|
|
160
|
+
|
|
161
|
+
# --- FIRE THE TRIGGER ---
|
|
162
|
+
ret = subprocess.run(["gh", "workflow", "run", WORKFLOW_NAME, "--ref", TARGET_REF], check=False)
|
|
163
|
+
|
|
164
|
+
if ret.returncode != 0:
|
|
165
|
+
print("::warning::Could not auto-trigger next run. Please retry the job manually.")
|
|
166
|
+
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write("changes_pushed=false\n")
|
|
170
|
+
print("Clean run.")
|
|
171
|
+
EOF
|
|
172
|
+
|
|
173
|
+
version:
|
|
174
|
+
needs: [ quality-check ]
|
|
175
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && github.ref == 'refs/heads/dev'
|
|
176
|
+
runs-on: ubuntu-latest
|
|
177
|
+
outputs:
|
|
178
|
+
version: ${{ steps.version.outputs.version }}
|
|
179
|
+
steps:
|
|
180
|
+
- uses: actions/checkout@v6
|
|
181
|
+
- uses: actions/setup-python@v5
|
|
182
|
+
with:
|
|
183
|
+
python-version: '3.12'
|
|
184
|
+
- uses: astral-sh/setup-uv@v5
|
|
185
|
+
with:
|
|
186
|
+
enable-cache: true
|
|
187
|
+
- id: version
|
|
188
|
+
run: echo "version=$(uv run python quasarr/providers/version.py)" >> $GITHUB_OUTPUT
|
|
189
|
+
|
|
190
|
+
build-wheel:
|
|
191
|
+
needs: [ quality-check, version ]
|
|
192
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && github.ref == 'refs/heads/dev'
|
|
193
|
+
runs-on: ubuntu-latest
|
|
194
|
+
outputs:
|
|
195
|
+
attestation-id: ${{ steps.attest.outputs.attestation-id }}
|
|
196
|
+
permissions:
|
|
197
|
+
contents: read
|
|
198
|
+
id-token: write
|
|
199
|
+
attestations: write
|
|
200
|
+
steps:
|
|
201
|
+
- uses: actions/checkout@v6
|
|
202
|
+
- uses: actions/setup-python@v5
|
|
203
|
+
with:
|
|
204
|
+
python-version: '3.12'
|
|
205
|
+
- uses: astral-sh/setup-uv@v5
|
|
206
|
+
with:
|
|
207
|
+
enable-cache: true
|
|
208
|
+
- run: uv build
|
|
209
|
+
- id: attest
|
|
210
|
+
uses: actions/attest-build-provenance@v2
|
|
211
|
+
with:
|
|
212
|
+
subject-path: "dist/*.whl"
|
|
213
|
+
- uses: actions/upload-artifact@v4
|
|
214
|
+
with:
|
|
215
|
+
name: wheel
|
|
216
|
+
path: ./dist/*
|
|
217
|
+
|
|
218
|
+
build-exe:
|
|
219
|
+
needs: [ quality-check, version ]
|
|
220
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && github.ref == 'refs/heads/dev'
|
|
221
|
+
runs-on: windows-latest
|
|
222
|
+
env:
|
|
223
|
+
TMP: "D:\\a\\temp"
|
|
224
|
+
TEMP: "D:\\a\\temp"
|
|
225
|
+
steps:
|
|
226
|
+
- run: mkdir D:\a\temp -Force
|
|
227
|
+
- uses: actions/checkout@v6
|
|
228
|
+
- uses: actions/setup-python@v5
|
|
229
|
+
with:
|
|
230
|
+
python-version: '3.12'
|
|
231
|
+
- uses: astral-sh/setup-uv@v5
|
|
232
|
+
with:
|
|
233
|
+
enable-cache: true
|
|
234
|
+
- shell: powershell
|
|
235
|
+
run: Set-MpPreference -DisableRealtimeMonitoring $true
|
|
236
|
+
- run: uv sync --group build
|
|
237
|
+
- run: |
|
|
238
|
+
uv run python -c "from PIL import Image; Image.open('Quasarr.png').save('Quasarr.ico')"
|
|
239
|
+
uv run python quasarr/providers/version.py --create-version-file
|
|
240
|
+
uv run pyinstaller --clean --onefile -y --version-file "file_version_info.txt" --icon "Quasarr.ico" "Quasarr.py" -n "quasarr-${{ needs.version.outputs.version }}-standalone-win64"
|
|
241
|
+
- uses: actions/upload-artifact@v4
|
|
242
|
+
with:
|
|
243
|
+
name: exe-amd64
|
|
244
|
+
path: ./dist/*.exe
|
|
245
|
+
|
|
246
|
+
beta-release:
|
|
247
|
+
needs: [ version, build-wheel, build-exe ]
|
|
248
|
+
runs-on: ubuntu-latest
|
|
249
|
+
permissions: { contents: write }
|
|
250
|
+
steps:
|
|
251
|
+
- uses: actions/checkout@v6
|
|
252
|
+
with:
|
|
253
|
+
fetch-depth: 0
|
|
254
|
+
- uses: actions/download-artifact@v4
|
|
255
|
+
with:
|
|
256
|
+
name: wheel
|
|
257
|
+
path: ./wheel
|
|
258
|
+
- uses: actions/download-artifact@v4
|
|
259
|
+
with:
|
|
260
|
+
name: exe-amd64
|
|
261
|
+
path: ./exe-amd64
|
|
262
|
+
- id: changelog
|
|
263
|
+
uses: metcalfc/changelog-generator@v4.6.2
|
|
264
|
+
with:
|
|
265
|
+
myToken: ${{ secrets.GITHUB_TOKEN }}
|
|
266
|
+
- name: Create Release Body
|
|
267
|
+
run: |
|
|
268
|
+
echo "### Docker:" > release_body.md
|
|
269
|
+
echo "\`docker pull ${{ env.GHCR_ENDPOINT }}:beta\`" >> release_body.md
|
|
270
|
+
echo "### Python:" >> release_body.md
|
|
271
|
+
echo "\`uv tool upgrade quasarr\`" >> release_body.md
|
|
272
|
+
echo "### Changelog:" >> release_body.md
|
|
273
|
+
echo "${{ steps.changelog.outputs.changelog }}" >> release_body.md
|
|
274
|
+
echo "[Attestation](https://github.com/${{ github.repository }}/attestations/${{ needs.build-wheel.outputs.attestation-id }})" >> release_body.md
|
|
275
|
+
- uses: ncipollo/release-action@v1
|
|
276
|
+
with:
|
|
277
|
+
artifacts: "./wheel/*.whl,./exe-amd64/*.exe"
|
|
278
|
+
allowUpdates: true
|
|
279
|
+
removeArtifacts: true
|
|
280
|
+
replacesArtifacts: true
|
|
281
|
+
tag: beta
|
|
282
|
+
name: Beta Build
|
|
283
|
+
bodyFile: "release_body.md"
|
|
284
|
+
prerelease: true
|
|
285
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
286
|
+
|
|
287
|
+
build-docker-amd64:
|
|
288
|
+
needs: [ quality-check, version, build-wheel ]
|
|
289
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && github.ref == 'refs/heads/dev'
|
|
290
|
+
runs-on: ubuntu-latest
|
|
291
|
+
steps:
|
|
292
|
+
- uses: actions/checkout@v6
|
|
293
|
+
- uses: actions/download-artifact@v4
|
|
294
|
+
with:
|
|
295
|
+
name: wheel
|
|
296
|
+
path: ./docker/dist
|
|
297
|
+
- uses: docker/setup-buildx-action@v3
|
|
298
|
+
- uses: docker/login-action@v3
|
|
299
|
+
with:
|
|
300
|
+
registry: ghcr.io
|
|
301
|
+
username: ${{ github.actor }}
|
|
302
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
303
|
+
- uses: docker/build-push-action@v6
|
|
304
|
+
with:
|
|
305
|
+
context: ./docker
|
|
306
|
+
platforms: linux/amd64
|
|
307
|
+
push: true
|
|
308
|
+
provenance: false
|
|
309
|
+
sbom: false
|
|
310
|
+
annotations: org.opencontainers.image.description=${{ env.DESCRIPTION }}
|
|
311
|
+
tags: |
|
|
312
|
+
${{ env.GHCR_ENDPOINT }}:beta-amd64
|
|
313
|
+
${{ env.GHCR_ENDPOINT }}:${{ needs.version.outputs.version }}-beta-amd64
|
|
314
|
+
build-args: VS=${{ needs.version.outputs.version }}
|
|
315
|
+
cache-from: type=gha,scope=beta-amd64
|
|
316
|
+
cache-to: type=gha,mode=max,scope=beta-amd64
|
|
317
|
+
|
|
318
|
+
build-docker-arm64:
|
|
319
|
+
needs: [ quality-check, version, build-wheel ]
|
|
320
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && github.ref == 'refs/heads/dev'
|
|
321
|
+
runs-on: ubuntu-24.04-arm
|
|
322
|
+
steps:
|
|
323
|
+
- uses: actions/checkout@v6
|
|
324
|
+
- uses: actions/download-artifact@v4
|
|
325
|
+
with:
|
|
326
|
+
name: wheel
|
|
327
|
+
path: ./docker/dist
|
|
328
|
+
- uses: docker/setup-buildx-action@v3
|
|
329
|
+
- uses: docker/login-action@v3
|
|
330
|
+
with:
|
|
331
|
+
registry: ghcr.io
|
|
332
|
+
username: ${{ github.actor }}
|
|
333
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
334
|
+
- uses: docker/build-push-action@v6
|
|
335
|
+
with:
|
|
336
|
+
context: ./docker
|
|
337
|
+
platforms: linux/arm64
|
|
338
|
+
push: true
|
|
339
|
+
provenance: false
|
|
340
|
+
sbom: false
|
|
341
|
+
annotations: org.opencontainers.image.description=${{ env.DESCRIPTION }}
|
|
342
|
+
tags: |
|
|
343
|
+
${{ env.GHCR_ENDPOINT }}:beta-arm64
|
|
344
|
+
${{ env.GHCR_ENDPOINT }}:${{ needs.version.outputs.version }}-beta-arm64
|
|
345
|
+
build-args: VS=${{ needs.version.outputs.version }}
|
|
346
|
+
cache-from: type=gha,scope=beta-arm64
|
|
347
|
+
cache-to: type=gha,mode=max,scope=beta-arm64
|
|
348
|
+
|
|
349
|
+
merge-docker-manifest:
|
|
350
|
+
needs: [ version, build-docker-amd64, build-docker-arm64 ]
|
|
351
|
+
runs-on: ubuntu-latest
|
|
352
|
+
steps:
|
|
353
|
+
- uses: docker/setup-buildx-action@v3
|
|
354
|
+
- uses: docker/login-action@v3
|
|
355
|
+
with:
|
|
356
|
+
registry: ghcr.io
|
|
357
|
+
username: ${{ github.actor }}
|
|
358
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
359
|
+
- run: |
|
|
360
|
+
TAG_AMD64="${{ needs.version.outputs.version }}-beta-amd64"
|
|
361
|
+
TAG_ARM64="${{ needs.version.outputs.version }}-beta-arm64"
|
|
362
|
+
ANNOTATION="index:org.opencontainers.image.description=$DESCRIPTION"
|
|
363
|
+
|
|
364
|
+
docker buildx imagetools create -t ${{ env.GHCR_ENDPOINT }}:beta \
|
|
365
|
+
--annotation "$ANNOTATION" \
|
|
366
|
+
${{ env.GHCR_ENDPOINT }}:beta-amd64 \
|
|
367
|
+
${{ env.GHCR_ENDPOINT }}:beta-arm64
|
|
368
|
+
|
|
369
|
+
docker buildx imagetools create -t ${{ env.GHCR_ENDPOINT }}:${{ needs.version.outputs.version }}-beta \
|
|
370
|
+
--annotation "$ANNOTATION" \
|
|
371
|
+
${{ env.GHCR_ENDPOINT }}:${TAG_AMD64} \
|
|
372
|
+
${{ env.GHCR_ENDPOINT }}:${TAG_ARM64}
|
|
373
|
+
|
|
374
|
+
notify:
|
|
375
|
+
name: Notify Discord & PR
|
|
376
|
+
needs: [ quality-check, version, beta-release, merge-docker-manifest ]
|
|
377
|
+
# Only run if the build wasn't skipped by the auto-fixer and the release succeeded
|
|
378
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && needs.beta-release.result == 'success'
|
|
379
|
+
runs-on: ubuntu-latest
|
|
380
|
+
permissions:
|
|
381
|
+
pull-requests: write # Required to comment on the PR
|
|
382
|
+
contents: read
|
|
383
|
+
steps:
|
|
384
|
+
- uses: actions/checkout@v6
|
|
385
|
+
|
|
386
|
+
- name: Send Notifications
|
|
387
|
+
env:
|
|
388
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
389
|
+
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
390
|
+
VERSION: ${{ needs.version.outputs.version }}
|
|
391
|
+
REPO: ${{ github.repository }}
|
|
392
|
+
run: |
|
|
393
|
+
echo "Fetching release details..."
|
|
394
|
+
# Get the body of the release we just created (beta tag)
|
|
395
|
+
RELEASE_BODY=$(gh release view beta --json body --jq .body)
|
|
396
|
+
|
|
397
|
+
# --- 1. DISCORD NOTIFICATION ---
|
|
398
|
+
if [ -n "$DISCORD_WEBHOOK" ]; then
|
|
399
|
+
echo "Sending Discord Webhook..."
|
|
400
|
+
|
|
401
|
+
# Construct a JSON payload safely using jq (handles escaping quotes/newlines)
|
|
402
|
+
# We create a simple embed with the version and the release body
|
|
403
|
+
jq -n \
|
|
404
|
+
--arg title "🚀 New Beta Build: $VERSION" \
|
|
405
|
+
--arg desc "$RELEASE_BODY" \
|
|
406
|
+
--arg url "https://github.com/$REPO/releases/tag/beta" \
|
|
407
|
+
'{content: null, embeds: [{title: $title, description: $desc, url: $url, color: 5814783}]}' \
|
|
408
|
+
> discord_payload.json
|
|
409
|
+
|
|
410
|
+
curl -H "Content-Type: application/json" \
|
|
411
|
+
-d @discord_payload.json \
|
|
412
|
+
"$DISCORD_WEBHOOK"
|
|
413
|
+
else
|
|
414
|
+
echo "::warning::Skipping Discord notification (DISCORD_WEBHOOK secret not set)"
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# --- 2. PR COMMENT FALLBACK ---
|
|
418
|
+
# Find the PR associated with the commit that triggered this push
|
|
419
|
+
echo "Looking for associated PR..."
|
|
420
|
+
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
|
|
421
|
+
|
|
422
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
423
|
+
echo "Found PR #$PR_NUMBER. Posting comment..."
|
|
424
|
+
|
|
425
|
+
echo "## 🚀 Beta Release $VERSION is Live!" > pr_comment.md
|
|
426
|
+
echo "" >> pr_comment.md
|
|
427
|
+
echo "$RELEASE_BODY" >> pr_comment.md
|
|
428
|
+
echo "" >> pr_comment.md
|
|
429
|
+
echo "[View Release on GitHub](https://github.com/$REPO/releases/tag/beta)" >> pr_comment.md
|
|
430
|
+
|
|
431
|
+
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
|
|
432
|
+
else
|
|
433
|
+
echo "No merged PR found for commit ${{ github.sha }}. Skipping PR comment."
|
|
434
|
+
fi
|
|
@@ -34,7 +34,6 @@ jobs:
|
|
|
34
34
|
uses: astral-sh/setup-uv@v5
|
|
35
35
|
with:
|
|
36
36
|
enable-cache: true
|
|
37
|
-
cache-dependency-glob: "pyproject.toml"
|
|
38
37
|
- name: Get Version
|
|
39
38
|
id: version
|
|
40
39
|
run: |
|
|
@@ -43,6 +42,8 @@ jobs:
|
|
|
43
42
|
build-wheel:
|
|
44
43
|
name: Build Wheel
|
|
45
44
|
runs-on: ubuntu-latest
|
|
45
|
+
outputs:
|
|
46
|
+
attestation-id: ${{ steps.attest.outputs.attestation-id }}
|
|
46
47
|
permissions:
|
|
47
48
|
contents: read
|
|
48
49
|
id-token: write
|
|
@@ -56,10 +57,10 @@ jobs:
|
|
|
56
57
|
uses: astral-sh/setup-uv@v5
|
|
57
58
|
with:
|
|
58
59
|
enable-cache: true
|
|
59
|
-
cache-dependency-glob: "pyproject.toml"
|
|
60
60
|
- name: Build wheel
|
|
61
61
|
run: uv build
|
|
62
62
|
- name: Generate artifact attestation
|
|
63
|
+
id: attest
|
|
63
64
|
uses: actions/attest-build-provenance@v2
|
|
64
65
|
with:
|
|
65
66
|
subject-path: "dist/*.whl"
|
|
@@ -87,7 +88,6 @@ jobs:
|
|
|
87
88
|
uses: astral-sh/setup-uv@v5
|
|
88
89
|
with:
|
|
89
90
|
enable-cache: true
|
|
90
|
-
cache-dependency-glob: "pyproject.toml"
|
|
91
91
|
|
|
92
92
|
- name: Disable Windows Defender
|
|
93
93
|
shell: powershell
|
|
@@ -249,7 +249,6 @@ jobs:
|
|
|
249
249
|
uses: astral-sh/setup-uv@v5
|
|
250
250
|
with:
|
|
251
251
|
enable-cache: true
|
|
252
|
-
cache-dependency-glob: "pyproject.toml"
|
|
253
252
|
|
|
254
253
|
- name: Publish to PyPI
|
|
255
254
|
if: ${{ !inputs.skip_pypi }}
|
|
@@ -268,15 +267,67 @@ jobs:
|
|
|
268
267
|
if [ -n "$PR_BODY" ]; then
|
|
269
268
|
echo -e "\n$PR_BODY" >> .github/Changelog.md
|
|
270
269
|
fi
|
|
271
|
-
- name:
|
|
270
|
+
- name: Create Release Body
|
|
272
271
|
run: |
|
|
273
|
-
echo
|
|
274
|
-
echo
|
|
272
|
+
echo "### Docker:" > release_body.md
|
|
273
|
+
echo "" >> release_body.md
|
|
274
|
+
echo "\`docker pull ${{ env.GHCR_ENDPOINT }}:latest\`" >> release_body.md
|
|
275
|
+
echo "" >> release_body.md
|
|
276
|
+
echo "### Python:" >> release_body.md
|
|
277
|
+
echo "" >> release_body.md
|
|
278
|
+
echo "\`uv tool upgrade quasarr\`" >> release_body.md
|
|
279
|
+
echo "" >> release_body.md
|
|
280
|
+
echo "### Changelog:" >> release_body.md
|
|
281
|
+
echo "" >> release_body.md
|
|
282
|
+
echo "${{ steps.changelog.outputs.changelog }}" >> release_body.md
|
|
283
|
+
echo "" >> release_body.md
|
|
284
|
+
echo "[Attestation](https://github.com/${{ github.repository }}/attestations/${{ needs.build-wheel.outputs.attestation-id }})" >> release_body.md
|
|
275
285
|
|
|
276
286
|
- name: Create Release
|
|
277
287
|
uses: ncipollo/release-action@v1
|
|
278
288
|
with:
|
|
279
289
|
artifacts: "./wheel/*.whl,./exe-amd64/*.exe"
|
|
280
290
|
artifactErrorsFailBuild: true
|
|
281
|
-
bodyFile: ".
|
|
291
|
+
bodyFile: "release_body.md"
|
|
282
292
|
tag: v.${{ needs.version.outputs.version }}
|
|
293
|
+
|
|
294
|
+
notify:
|
|
295
|
+
name: Notify Discord
|
|
296
|
+
needs: [ version, release, merge-docker-manifest ]
|
|
297
|
+
if: always() && needs.release.result == 'success'
|
|
298
|
+
runs-on: ubuntu-latest
|
|
299
|
+
steps:
|
|
300
|
+
- uses: actions/checkout@v6
|
|
301
|
+
|
|
302
|
+
- name: Send Discord Webhook
|
|
303
|
+
env:
|
|
304
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
305
|
+
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
306
|
+
VERSION: ${{ needs.version.outputs.version }}
|
|
307
|
+
REPO: ${{ github.repository }}
|
|
308
|
+
run: |
|
|
309
|
+
# Use the exact tag format defined in the release job (v.X.X.X)
|
|
310
|
+
TAG="v.$VERSION"
|
|
311
|
+
echo "Fetching release details for $TAG..."
|
|
312
|
+
|
|
313
|
+
# Fetch the body from the release we just created
|
|
314
|
+
RELEASE_BODY=$(gh release view "$TAG" --json body --jq .body)
|
|
315
|
+
|
|
316
|
+
if [ -n "$DISCORD_WEBHOOK" ]; then
|
|
317
|
+
echo "Sending notification..."
|
|
318
|
+
|
|
319
|
+
# Construct JSON payload using jq
|
|
320
|
+
# Color 5763719 is 'Green'
|
|
321
|
+
jq -n \
|
|
322
|
+
--arg title "🚀 New Release: $TAG" \
|
|
323
|
+
--arg desc "$RELEASE_BODY" \
|
|
324
|
+
--arg url "https://github.com/$REPO/releases/tag/$TAG" \
|
|
325
|
+
'{content: null, embeds: [{title: $title, description: $desc, url: $url, color: 5763719}]}' \
|
|
326
|
+
> discord_payload.json
|
|
327
|
+
|
|
328
|
+
curl -H "Content-Type: application/json" \
|
|
329
|
+
-d @discord_payload.json \
|
|
330
|
+
"$DISCORD_WEBHOOK"
|
|
331
|
+
else
|
|
332
|
+
echo "::warning::Skipping Discord notification (DISCORD_WEBHOOK secret not set)"
|
|
333
|
+
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quasarr
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.9
|
|
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,7 +11,7 @@ Requires-Python: >=3.12
|
|
|
11
11
|
Requires-Dist: beautifulsoup4>=4.14.3
|
|
12
12
|
Requires-Dist: bottle>=0.13.4
|
|
13
13
|
Requires-Dist: dukpy>=0.5.0
|
|
14
|
-
Requires-Dist: pillow>=12.
|
|
14
|
+
Requires-Dist: pillow>=12.1.0
|
|
15
15
|
Requires-Dist: pycryptodomex>=3.23.0
|
|
16
16
|
Requires-Dist: requests>=2.32.5
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
@@ -13,13 +13,14 @@ COPY --from=uv /uv /usr/local/bin/uv
|
|
|
13
13
|
# install local package
|
|
14
14
|
COPY dist/*.whl /tmp/
|
|
15
15
|
|
|
16
|
-
#
|
|
16
|
+
# Configure uv to install tools in globally accessible paths
|
|
17
|
+
ENV UV_TOOL_DIR=/opt/uv-tools
|
|
18
|
+
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
|
19
|
+
|
|
20
|
+
# The binary will now automatically appear in /usr/local/bin
|
|
17
21
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
18
22
|
uv tool install /tmp/*.whl --force && rm /tmp/*.whl
|
|
19
23
|
|
|
20
|
-
# Ensure the binary is in the PATH
|
|
21
|
-
ENV PATH="/root/.local/bin:$PATH"
|
|
22
|
-
|
|
23
24
|
# volumes and ports
|
|
24
25
|
VOLUME /config
|
|
25
26
|
EXPOSE 8080
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Development Setup for Pull Requests
|
|
2
|
+
|
|
3
|
+
To test your changes before submitting a pull request:
|
|
4
|
+
|
|
5
|
+
**1. Prepare your Environment with `uv`**
|
|
6
|
+
|
|
7
|
+
Ensure you have the development tools (like `ruff`) installed and your environment synced:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync --group dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**2. Run Quasarr with the `--internal_address` parameter**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run Quasarr.py --internal_address=http://<host-ip>:<port>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Replace `<host-ip>` and `<port>` with the scheme, IP, and port of your host machine.
|
|
20
|
+
The `--internal_address` parameter is **mandatory**.
|
|
21
|
+
|
|
22
|
+
**3. Start the required services using the `dev-services-compose.yml` file**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
CONFIG_VOLUMES=/path/to/config docker-compose -f docker/dev-services-compose.yml up
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Replace `/path/to/config` with your desired configuration location.
|
|
29
|
+
The `CONFIG_VOLUMES` environment variable is **mandatory**.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### Code Quality & Maintenance
|
|
34
|
+
|
|
35
|
+
The CI pipeline enforces strict code styling and import optimization. Please run these commands before pushing your
|
|
36
|
+
changes:
|
|
37
|
+
|
|
38
|
+
**Optimize Imports and Fix Linting:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv run ruff check --fix .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Format Code Layout:**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv run ruff format .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Update Dependencies:**
|
|
51
|
+
To update the project lockfile to the latest versions of all packages without manually editing `pyproject.toml`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv lock --upgrade
|
|
55
|
+
```
|