quasarr 2.4.1__tar.gz → 2.4.5__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.
- quasarr-2.4.5/.github/Changelog.md +5 -0
- quasarr-2.4.5/.github/FUNDING.yml +1 -0
- quasarr-2.4.5/.github/ISSUE_TEMPLATE/bug_report.yml +67 -0
- quasarr-2.4.5/.github/ISSUE_TEMPLATE/config.yml +5 -0
- quasarr-2.4.5/.github/workflows/Beta.yml +191 -0
- quasarr-2.4.5/.github/workflows/HostnameRedaction.yml +256 -0
- quasarr-2.4.5/.github/workflows/PrVersionBumpCheck.yml +138 -0
- quasarr-2.4.5/.github/workflows/Release.yml +272 -0
- quasarr-2.4.5/.gitignore +17 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/PKG-INFO +11 -20
- quasarr-2.4.5/Quasarr.png +0 -0
- quasarr-2.4.5/Quasarr.py +11 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/README.md +5 -5
- quasarr-2.4.5/docker/Dockerfile +34 -0
- quasarr-2.4.5/docker/dev-services-compose.yml +98 -0
- quasarr-2.4.5/docker/dev-setup.md +21 -0
- quasarr-2.4.5/docker/docker-compose.yml +19 -0
- quasarr-2.4.5/pyproject.toml +40 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/__init__.py +9 -3
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/__init__.py +6 -3
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/captcha/__init__.py +11 -7
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/packages/__init__.py +135 -38
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/statistics/__init__.py +20 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/packages/__init__.py +8 -2
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/auth.py +1 -1
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/html_images.py +2 -1
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/html_templates.py +1 -1
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/imdb_metadata.py +1 -1
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/sessions/dd.py +1 -1
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/statistics.py +49 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/version.py +12 -4
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/web_server.py +1 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/he.py +2 -2
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/storage/setup.py +40 -15
- quasarr-2.4.1/quasarr.egg-info/PKG-INFO +0 -338
- quasarr-2.4.1/quasarr.egg-info/SOURCES.txt +0 -86
- quasarr-2.4.1/quasarr.egg-info/dependency_links.txt +0 -1
- quasarr-2.4.1/quasarr.egg-info/entry_points.txt +0 -2
- quasarr-2.4.1/quasarr.egg-info/not-zip-safe +0 -1
- quasarr-2.4.1/quasarr.egg-info/requires.txt +0 -6
- quasarr-2.4.1/quasarr.egg-info/top_level.txt +0 -1
- quasarr-2.4.1/setup.cfg +0 -4
- quasarr-2.4.1/setup.py +0 -43
- {quasarr-2.4.1 → quasarr-2.4.5}/LICENSE +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/arr/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/config/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/api/sponsors_helper/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/al.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/hide.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/al.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/by.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/dd.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/dj.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/dl.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/dt.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/dw.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/he.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/mb.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/nk.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/nx.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/sf.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/sj.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/sl.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/wd.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/downloads/sources/wx.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/cloudflare.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/hostname_issues.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/jd_cache.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/log.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/myjd_api.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/notifications.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/obfuscated.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/sessions/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/sessions/al.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/sessions/dl.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/sessions/nx.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/shared_state.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/providers/utils.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/al.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/by.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/dd.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/dj.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/dl.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/dt.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/dw.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/fx.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/mb.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/nk.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/nx.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/sf.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/sj.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/sl.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/wd.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/search/sources/wx.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/storage/__init__.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/storage/config.py +0 -0
- {quasarr-2.4.1 → quasarr-2.4.5}/quasarr/storage/sqlite_database.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: rix1337
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Create a bug report
|
|
3
|
+
body:
|
|
4
|
+
- type: checkboxes
|
|
5
|
+
attributes:
|
|
6
|
+
label: Did you read the README?
|
|
7
|
+
description: Please read the <a href="https://github.com/rix1337/Quasarr/blob/master/README.md">README</a> first.
|
|
8
|
+
options:
|
|
9
|
+
- label: I have read the README.
|
|
10
|
+
required: true
|
|
11
|
+
- type: checkboxes
|
|
12
|
+
attributes:
|
|
13
|
+
label: Is this an existing issue?
|
|
14
|
+
description: Check <a href="https://github.com/rix1337/Quasarr/issues?q=is%3Aissue">all issues</a> to prevent duplicates.
|
|
15
|
+
options:
|
|
16
|
+
- label: I confirm this is not a duplicate.
|
|
17
|
+
required: true
|
|
18
|
+
- type: checkboxes
|
|
19
|
+
attributes:
|
|
20
|
+
label: Are hostnames part of this issue?
|
|
21
|
+
description: The author does not provide any hostnames. You personally decide what sites to use in Quasarr.
|
|
22
|
+
options:
|
|
23
|
+
- label: No, hostnames are not part of this issue. I am also not asking for any clarification here!
|
|
24
|
+
required: true
|
|
25
|
+
- type: textarea
|
|
26
|
+
attributes:
|
|
27
|
+
label: Environment
|
|
28
|
+
description: Tell us about your setup
|
|
29
|
+
value: |
|
|
30
|
+
- Quasarr version:
|
|
31
|
+
- Last working version:
|
|
32
|
+
- Type: [Docker/Windows-Exe/Manual]
|
|
33
|
+
- OS: [Docker/Windows/Linux/macOS]
|
|
34
|
+
render: markdown
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
- type: textarea
|
|
38
|
+
attributes:
|
|
39
|
+
label: Description
|
|
40
|
+
description: Describe all steps to reproduce the issue. Without these, noone will be able to help you!
|
|
41
|
+
validations:
|
|
42
|
+
required: true
|
|
43
|
+
- type: textarea
|
|
44
|
+
attributes:
|
|
45
|
+
label: Error from the console
|
|
46
|
+
description: |
|
|
47
|
+
Add ALL messages from the log that correlate with your issue.
|
|
48
|
+
Redact all hostnames you may have set.
|
|
49
|
+
If you can't see a log message that correlates with your issue, set the DEBUG environment variable to "True".
|
|
50
|
+
render: text
|
|
51
|
+
validations:
|
|
52
|
+
required: true
|
|
53
|
+
- type: textarea
|
|
54
|
+
attributes:
|
|
55
|
+
label: Quasarr.ini
|
|
56
|
+
description: |
|
|
57
|
+
To reproduce your issue we need to know about your setup.
|
|
58
|
+
Hostnames and credentials are encrypted in the ini. Never share your Quasarr.db to keep them secure!
|
|
59
|
+
render: text
|
|
60
|
+
validations:
|
|
61
|
+
required: true
|
|
62
|
+
- type: textarea
|
|
63
|
+
attributes:
|
|
64
|
+
label: Screenshots
|
|
65
|
+
description: Add screenshots that show your issue (ideally log and UI e.g. of Radarr/Sonarr)
|
|
66
|
+
validations:
|
|
67
|
+
required: false
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
name: Beta Docker Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- dev
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
GHCR_ENDPOINT: "ghcr.io/rix1337/quasarr"
|
|
11
|
+
DESCRIPTION: "Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs."
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
version:
|
|
15
|
+
name: Get Version
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
outputs:
|
|
18
|
+
version: ${{ steps.version.outputs.version }}
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.12'
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v5
|
|
26
|
+
with:
|
|
27
|
+
# Fixed: Added glob to silence warning since you might not have uv.lock
|
|
28
|
+
enable-cache: true
|
|
29
|
+
cache-dependency-glob: "pyproject.toml"
|
|
30
|
+
- name: Get Version
|
|
31
|
+
id: version
|
|
32
|
+
run: |
|
|
33
|
+
echo "version=$(uv run python quasarr/providers/version.py)" >> $GITHUB_OUTPUT
|
|
34
|
+
|
|
35
|
+
build-wheel:
|
|
36
|
+
name: Build Wheel
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v6
|
|
40
|
+
- uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: '3.12'
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v5
|
|
45
|
+
with:
|
|
46
|
+
enable-cache: true
|
|
47
|
+
cache-dependency-glob: "pyproject.toml"
|
|
48
|
+
- name: Build wheel
|
|
49
|
+
run: uv build
|
|
50
|
+
- uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: wheel
|
|
53
|
+
path: ./dist/*
|
|
54
|
+
|
|
55
|
+
build-exe:
|
|
56
|
+
name: Build Exe (Windows)
|
|
57
|
+
runs-on: windows-latest
|
|
58
|
+
needs: version
|
|
59
|
+
env:
|
|
60
|
+
TMP: D:\a\temp
|
|
61
|
+
TEMP: D:\a\temp
|
|
62
|
+
steps:
|
|
63
|
+
- name: Create Temp Dir
|
|
64
|
+
run: mkdir D:\a\temp -Force
|
|
65
|
+
- uses: actions/checkout@v6
|
|
66
|
+
- uses: actions/setup-python@v5
|
|
67
|
+
with:
|
|
68
|
+
python-version: '3.12'
|
|
69
|
+
- name: Install uv
|
|
70
|
+
uses: astral-sh/setup-uv@v5
|
|
71
|
+
with:
|
|
72
|
+
enable-cache: true
|
|
73
|
+
cache-dependency-glob: "pyproject.toml"
|
|
74
|
+
- uses: actions/cache@v4
|
|
75
|
+
with:
|
|
76
|
+
path: ~\AppData\Local\pyinstaller
|
|
77
|
+
key: ${{ runner.os }}-pyinstaller-${{ hashFiles('pyproject.toml') }}
|
|
78
|
+
restore-keys: ${{ runner.os }}-pyinstaller-
|
|
79
|
+
- name: Disable Windows Defender
|
|
80
|
+
shell: powershell
|
|
81
|
+
run: Set-MpPreference -DisableRealtimeMonitoring $true
|
|
82
|
+
- name: Install dependencies
|
|
83
|
+
run: |
|
|
84
|
+
uv sync --group build
|
|
85
|
+
- name: Build exe
|
|
86
|
+
run: |
|
|
87
|
+
uv run python quasarr/providers/version.py --create-version-file
|
|
88
|
+
uv run pyinstaller --clean --onefile -y --version-file "file_version_info.txt" "Quasarr.py" -n "quasarr-${{ needs.version.outputs.version }}-standalone-win64"
|
|
89
|
+
- uses: actions/upload-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: exe-amd64
|
|
92
|
+
path: ./dist/*.exe
|
|
93
|
+
|
|
94
|
+
build-docker-amd64:
|
|
95
|
+
name: Build Docker (AMD64) :beta
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
needs: [ version, build-wheel ]
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v6
|
|
100
|
+
- uses: actions/download-artifact@v4
|
|
101
|
+
with:
|
|
102
|
+
name: wheel
|
|
103
|
+
path: ./docker/dist
|
|
104
|
+
- uses: docker/setup-buildx-action@v3
|
|
105
|
+
- uses: docker/login-action@v3
|
|
106
|
+
with:
|
|
107
|
+
registry: ghcr.io
|
|
108
|
+
username: ${{ github.actor }}
|
|
109
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
110
|
+
- name: Build and Push
|
|
111
|
+
uses: docker/build-push-action@v6
|
|
112
|
+
with:
|
|
113
|
+
context: ./docker
|
|
114
|
+
platforms: linux/amd64
|
|
115
|
+
push: true
|
|
116
|
+
provenance: false
|
|
117
|
+
sbom: false
|
|
118
|
+
annotations: |
|
|
119
|
+
org.opencontainers.image.description=${{ env.DESCRIPTION }}
|
|
120
|
+
tags: |
|
|
121
|
+
${{ env.GHCR_ENDPOINT }}:beta-amd64
|
|
122
|
+
${{ env.GHCR_ENDPOINT }}:${{ needs.version.outputs.version }}-beta-amd64
|
|
123
|
+
build-args: VS=${{ needs.version.outputs.version }}
|
|
124
|
+
cache-from: type=gha,scope=beta-amd64
|
|
125
|
+
cache-to: type=gha,mode=max,scope=beta-amd64
|
|
126
|
+
|
|
127
|
+
build-docker-arm64:
|
|
128
|
+
name: Build Docker (ARM64) :beta
|
|
129
|
+
runs-on: ubuntu-24.04-arm
|
|
130
|
+
needs: [ version, build-wheel ]
|
|
131
|
+
steps:
|
|
132
|
+
- uses: actions/checkout@v6
|
|
133
|
+
- uses: actions/download-artifact@v4
|
|
134
|
+
with:
|
|
135
|
+
name: wheel
|
|
136
|
+
path: ./docker/dist
|
|
137
|
+
- uses: docker/setup-buildx-action@v3
|
|
138
|
+
- uses: docker/login-action@v3
|
|
139
|
+
with:
|
|
140
|
+
registry: ghcr.io
|
|
141
|
+
username: ${{ github.actor }}
|
|
142
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
143
|
+
- name: Build and Push
|
|
144
|
+
uses: docker/build-push-action@v6
|
|
145
|
+
with:
|
|
146
|
+
context: ./docker
|
|
147
|
+
platforms: linux/arm64
|
|
148
|
+
push: true
|
|
149
|
+
provenance: false
|
|
150
|
+
sbom: false
|
|
151
|
+
annotations: |
|
|
152
|
+
org.opencontainers.image.description=${{ env.DESCRIPTION }}
|
|
153
|
+
tags: |
|
|
154
|
+
${{ env.GHCR_ENDPOINT }}:beta-arm64
|
|
155
|
+
${{ env.GHCR_ENDPOINT }}:${{ needs.version.outputs.version }}-beta-arm64
|
|
156
|
+
build-args: VS=${{ needs.version.outputs.version }}
|
|
157
|
+
cache-from: type=gha,scope=beta-arm64
|
|
158
|
+
cache-to: type=gha,mode=max,scope=beta-arm64
|
|
159
|
+
|
|
160
|
+
merge-docker-manifest:
|
|
161
|
+
name: Merge Docker Manifests
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
needs: [ version, build-docker-amd64, build-docker-arm64 ]
|
|
164
|
+
steps:
|
|
165
|
+
# Fixed: Added setup-buildx-action to ensure 'imagetools' supports annotations correctly
|
|
166
|
+
- uses: docker/setup-buildx-action@v3
|
|
167
|
+
- uses: docker/login-action@v3
|
|
168
|
+
with:
|
|
169
|
+
registry: ghcr.io
|
|
170
|
+
username: ${{ github.actor }}
|
|
171
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
172
|
+
- name: Create and Push Manifests
|
|
173
|
+
run: |
|
|
174
|
+
# 1. Define source tags
|
|
175
|
+
TAG_AMD64="${{ needs.version.outputs.version }}-beta-amd64"
|
|
176
|
+
TAG_ARM64="${{ needs.version.outputs.version }}-beta-arm64"
|
|
177
|
+
|
|
178
|
+
# Fixed: Use shell variable directly and 'index:' prefix to strictly target the manifest list
|
|
179
|
+
ANNOTATION="index:org.opencontainers.image.description=$DESCRIPTION"
|
|
180
|
+
|
|
181
|
+
# 2. Create manifest for GHCR :beta
|
|
182
|
+
docker buildx imagetools create -t ${{ env.GHCR_ENDPOINT }}:beta \
|
|
183
|
+
--annotation "$ANNOTATION" \
|
|
184
|
+
${{ env.GHCR_ENDPOINT }}:beta-amd64 \
|
|
185
|
+
${{ env.GHCR_ENDPOINT }}:beta-arm64
|
|
186
|
+
|
|
187
|
+
# 3. Create manifest for GHCR :version-beta
|
|
188
|
+
docker buildx imagetools create -t ${{ env.GHCR_ENDPOINT }}:${{ needs.version.outputs.version }}-beta \
|
|
189
|
+
--annotation "$ANNOTATION" \
|
|
190
|
+
${{ env.GHCR_ENDPOINT }}:${TAG_AMD64} \
|
|
191
|
+
${{ env.GHCR_ENDPOINT }}:${TAG_ARM64}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
name: Hostname Redaction
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types: [ opened, edited ]
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [ opened, edited ]
|
|
8
|
+
issue_comment:
|
|
9
|
+
types: [ created, edited ]
|
|
10
|
+
pull_request_review_comment:
|
|
11
|
+
types: [ created, edited ]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
redact-hostnames:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: Redact hostnames
|
|
22
|
+
env:
|
|
23
|
+
GH_TOKEN: ${{ github.token }}
|
|
24
|
+
HOSTNAMES_URL: ${{ secrets.HOSTNAMES_URL }}
|
|
25
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
26
|
+
REPO: ${{ github.repository }}
|
|
27
|
+
SENDER: ${{ github.event.sender.login }}
|
|
28
|
+
# Issue fields
|
|
29
|
+
ISSUE_BODY: ${{ github.event.issue.body }}
|
|
30
|
+
ISSUE_TITLE: ${{ github.event.issue.title }}
|
|
31
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
32
|
+
# PR fields
|
|
33
|
+
PR_BODY: ${{ github.event.pull_request.body }}
|
|
34
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
35
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
36
|
+
# Comment fields
|
|
37
|
+
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
38
|
+
COMMENT_ID: ${{ github.event.comment.id }}
|
|
39
|
+
run: |
|
|
40
|
+
python - <<'EOF'
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import re
|
|
44
|
+
import subprocess
|
|
45
|
+
import urllib.request
|
|
46
|
+
|
|
47
|
+
# Load environment
|
|
48
|
+
HOSTNAMES_URL = os.environ.get("HOSTNAMES_URL", "")
|
|
49
|
+
EVENT_NAME = os.environ.get("EVENT_NAME", "")
|
|
50
|
+
REPO = os.environ.get("REPO", "")
|
|
51
|
+
SENDER = os.environ.get("SENDER", "")
|
|
52
|
+
|
|
53
|
+
ISSUE_BODY = os.environ.get("ISSUE_BODY") or ""
|
|
54
|
+
ISSUE_TITLE = os.environ.get("ISSUE_TITLE") or ""
|
|
55
|
+
ISSUE_NUMBER = os.environ.get("ISSUE_NUMBER") or ""
|
|
56
|
+
|
|
57
|
+
PR_BODY = os.environ.get("PR_BODY") or ""
|
|
58
|
+
PR_TITLE = os.environ.get("PR_TITLE") or ""
|
|
59
|
+
PR_NUMBER = os.environ.get("PR_NUMBER") or ""
|
|
60
|
+
|
|
61
|
+
COMMENT_BODY = os.environ.get("COMMENT_BODY") or ""
|
|
62
|
+
COMMENT_ID = os.environ.get("COMMENT_ID") or ""
|
|
63
|
+
|
|
64
|
+
# Determine event type
|
|
65
|
+
is_pr_event = EVENT_NAME == "pull_request"
|
|
66
|
+
is_issue_event = EVENT_NAME == "issues"
|
|
67
|
+
is_issue_comment = EVENT_NAME == "issue_comment"
|
|
68
|
+
is_pr_review_comment = EVENT_NAME == "pull_request_review_comment"
|
|
69
|
+
is_comment = is_issue_comment or is_pr_review_comment
|
|
70
|
+
|
|
71
|
+
# Get the right number for commenting
|
|
72
|
+
if is_pr_event or is_pr_review_comment:
|
|
73
|
+
NUMBER = PR_NUMBER
|
|
74
|
+
else:
|
|
75
|
+
NUMBER = ISSUE_NUMBER
|
|
76
|
+
|
|
77
|
+
# Prevent infinite loop when the action itself edits
|
|
78
|
+
if SENDER.endswith("[bot]"):
|
|
79
|
+
print(f"Edit by bot ({SENDER}), skipping")
|
|
80
|
+
exit(0)
|
|
81
|
+
|
|
82
|
+
if not HOSTNAMES_URL:
|
|
83
|
+
print("HOSTNAMES_URL secret not set, skipping")
|
|
84
|
+
exit(0)
|
|
85
|
+
|
|
86
|
+
if not NUMBER:
|
|
87
|
+
print("Could not determine issue/PR number, skipping")
|
|
88
|
+
exit(0)
|
|
89
|
+
|
|
90
|
+
# Fetch hostname list
|
|
91
|
+
try:
|
|
92
|
+
req = urllib.request.Request(HOSTNAMES_URL, headers={"User-Agent": "Mozilla/5.0"})
|
|
93
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
94
|
+
hostnames_data = resp.read().decode("utf-8")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"Failed to fetch hostnames: {e}")
|
|
97
|
+
exit(1)
|
|
98
|
+
|
|
99
|
+
# Parse hostname list into domain_base -> alias mapping
|
|
100
|
+
domain_to_alias = {}
|
|
101
|
+
for line in hostnames_data.strip().splitlines():
|
|
102
|
+
if "=" not in line:
|
|
103
|
+
continue
|
|
104
|
+
alias, hostname = line.split("=", 1)
|
|
105
|
+
alias = alias.strip()
|
|
106
|
+
hostname = hostname.strip()
|
|
107
|
+
if "." in hostname:
|
|
108
|
+
domain_base = hostname.rsplit(".", 1)[0]
|
|
109
|
+
domain_to_alias[domain_base.lower()] = alias
|
|
110
|
+
|
|
111
|
+
if not domain_to_alias:
|
|
112
|
+
print("No hostnames parsed, skipping")
|
|
113
|
+
exit(0)
|
|
114
|
+
|
|
115
|
+
# Build regex pattern to match domains with any TLD
|
|
116
|
+
escaped_domains = [re.escape(d) for d in domain_to_alias.keys()]
|
|
117
|
+
pattern = re.compile(
|
|
118
|
+
r'\b(' + '|'.join(escaped_domains) + r')\.[a-z]{2,}(?![a-z.])',
|
|
119
|
+
re.IGNORECASE
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
redacted_count = 0
|
|
123
|
+
redacted_aliases = set()
|
|
124
|
+
|
|
125
|
+
def replace_hostname(match):
|
|
126
|
+
global redacted_count
|
|
127
|
+
domain_base = match.group(1).lower()
|
|
128
|
+
alias = domain_to_alias.get(domain_base, "??")
|
|
129
|
+
redacted_count += 1
|
|
130
|
+
redacted_aliases.add(alias)
|
|
131
|
+
return f"({alias} redacted)"
|
|
132
|
+
|
|
133
|
+
def post_warning(count, target_type):
|
|
134
|
+
"""Post a warning comment about redacted hostnames."""
|
|
135
|
+
if count == 1:
|
|
136
|
+
msg = f"⚠️ **1 hostname was automatically redacted from this {target_type}.**"
|
|
137
|
+
else:
|
|
138
|
+
msg = f"⚠️ **{count} hostnames were automatically redacted from this {target_type}.**"
|
|
139
|
+
msg += "\n\nPlease use two-letter aliases (e.g. `al`, `dd`, `nx`) instead of actual hostnames."
|
|
140
|
+
|
|
141
|
+
if is_pr_event or is_pr_review_comment:
|
|
142
|
+
subprocess.run(
|
|
143
|
+
["gh", "pr", "comment", NUMBER, "--body", msg, "-R", REPO],
|
|
144
|
+
check=True
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
subprocess.run(
|
|
148
|
+
["gh", "issue", "comment", NUMBER, "--body", msg, "-R", REPO],
|
|
149
|
+
check=True
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Handle comments (issue comments, PR comments, PR review comments)
|
|
153
|
+
if is_comment:
|
|
154
|
+
if not COMMENT_BODY:
|
|
155
|
+
print("Comment is empty, skipping")
|
|
156
|
+
exit(0)
|
|
157
|
+
|
|
158
|
+
matches = pattern.findall(COMMENT_BODY)
|
|
159
|
+
|
|
160
|
+
if not matches:
|
|
161
|
+
print("No hostnames found in comment")
|
|
162
|
+
exit(0)
|
|
163
|
+
|
|
164
|
+
# Count unique aliases found
|
|
165
|
+
for domain_base in matches:
|
|
166
|
+
alias = domain_to_alias.get(domain_base.lower(), "??")
|
|
167
|
+
redacted_count += 1
|
|
168
|
+
redacted_aliases.add(alias)
|
|
169
|
+
|
|
170
|
+
print(f"Found {redacted_count} hostname(s) in comment: {', '.join(sorted(redacted_aliases))}")
|
|
171
|
+
|
|
172
|
+
# Delete the comment (editing leaves visible history)
|
|
173
|
+
if is_pr_review_comment:
|
|
174
|
+
api_endpoint = f"/repos/{REPO}/pulls/comments/{COMMENT_ID}"
|
|
175
|
+
else:
|
|
176
|
+
api_endpoint = f"/repos/{REPO}/issues/comments/{COMMENT_ID}"
|
|
177
|
+
|
|
178
|
+
subprocess.run(
|
|
179
|
+
["gh", "api", "--method", "DELETE", api_endpoint],
|
|
180
|
+
check=True
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Post explanation
|
|
184
|
+
if redacted_count == 1:
|
|
185
|
+
msg = f"🗑️ **A comment was automatically deleted because it contained a hostname.**"
|
|
186
|
+
else:
|
|
187
|
+
msg = f"🗑️ **A comment was automatically deleted because it contained {redacted_count} hostnames.**"
|
|
188
|
+
msg += "\n\nPlease repost using two-letter aliases (e.g. `al`, `dd`, `nx`) instead of actual hostnames."
|
|
189
|
+
|
|
190
|
+
if is_pr_review_comment:
|
|
191
|
+
subprocess.run(
|
|
192
|
+
["gh", "pr", "comment", NUMBER, "--body", msg, "-R", REPO],
|
|
193
|
+
check=True
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
subprocess.run(
|
|
197
|
+
["gh", "issue", "comment", NUMBER, "--body", msg, "-R", REPO],
|
|
198
|
+
check=True
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Handle issues
|
|
202
|
+
elif is_issue_event:
|
|
203
|
+
if not ISSUE_TITLE and not ISSUE_BODY:
|
|
204
|
+
print("Issue is empty, skipping")
|
|
205
|
+
exit(0)
|
|
206
|
+
|
|
207
|
+
new_title = pattern.sub(replace_hostname, ISSUE_TITLE)
|
|
208
|
+
new_body = pattern.sub(replace_hostname, ISSUE_BODY)
|
|
209
|
+
|
|
210
|
+
if redacted_count == 0:
|
|
211
|
+
print("No hostnames found in issue")
|
|
212
|
+
exit(0)
|
|
213
|
+
|
|
214
|
+
print(f"Redacted {redacted_count} hostname(s) from issue: {', '.join(sorted(redacted_aliases))}")
|
|
215
|
+
|
|
216
|
+
with open("new_body.md", "w") as f:
|
|
217
|
+
f.write(new_body)
|
|
218
|
+
|
|
219
|
+
cmd = ["gh", "issue", "edit", NUMBER, "--body-file", "new_body.md", "-R", REPO]
|
|
220
|
+
if new_title != ISSUE_TITLE:
|
|
221
|
+
cmd.extend(["--title", new_title])
|
|
222
|
+
|
|
223
|
+
subprocess.run(cmd, check=True)
|
|
224
|
+
post_warning(redacted_count, "issue")
|
|
225
|
+
|
|
226
|
+
# Handle pull requests
|
|
227
|
+
elif is_pr_event:
|
|
228
|
+
if not PR_TITLE and not PR_BODY:
|
|
229
|
+
print("PR is empty, skipping")
|
|
230
|
+
exit(0)
|
|
231
|
+
|
|
232
|
+
new_title = pattern.sub(replace_hostname, PR_TITLE)
|
|
233
|
+
new_body = pattern.sub(replace_hostname, PR_BODY)
|
|
234
|
+
|
|
235
|
+
if redacted_count == 0:
|
|
236
|
+
print("No hostnames found in PR")
|
|
237
|
+
exit(0)
|
|
238
|
+
|
|
239
|
+
print(f"Redacted {redacted_count} hostname(s) from PR: {', '.join(sorted(redacted_aliases))}")
|
|
240
|
+
|
|
241
|
+
with open("new_body.md", "w") as f:
|
|
242
|
+
f.write(new_body)
|
|
243
|
+
|
|
244
|
+
cmd = ["gh", "pr", "edit", NUMBER, "--body-file", "new_body.md", "-R", REPO]
|
|
245
|
+
if new_title != PR_TITLE:
|
|
246
|
+
cmd.extend(["--title", new_title])
|
|
247
|
+
|
|
248
|
+
subprocess.run(cmd, check=True)
|
|
249
|
+
post_warning(redacted_count, "pull request")
|
|
250
|
+
|
|
251
|
+
else:
|
|
252
|
+
print(f"Unknown event type: {EVENT_NAME}")
|
|
253
|
+
exit(0)
|
|
254
|
+
|
|
255
|
+
print("Done")
|
|
256
|
+
EOF
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
name: PR Version Bump Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [ opened, synchronize, reopened ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
version-check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pull-requests: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout PR branch
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
|
|
25
|
+
- name: Validate version bump
|
|
26
|
+
run: |
|
|
27
|
+
python - <<'EOF'
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
VERSION_FILE = Path("quasarr/providers/version.py")
|
|
35
|
+
GITHUB_STEP_SUMMARY = os.environ.get("GITHUB_STEP_SUMMARY", "")
|
|
36
|
+
COMMENT_FILE = Path("pr_comment.md")
|
|
37
|
+
|
|
38
|
+
def write_summary(content: str):
|
|
39
|
+
if GITHUB_STEP_SUMMARY:
|
|
40
|
+
with open(GITHUB_STEP_SUMMARY, "a") as f:
|
|
41
|
+
f.write(content + "\n")
|
|
42
|
+
with open(COMMENT_FILE, "a") as f:
|
|
43
|
+
f.write(content + "\n")
|
|
44
|
+
|
|
45
|
+
def gh_error(msg: str):
|
|
46
|
+
print(f"::error::{msg}")
|
|
47
|
+
|
|
48
|
+
def gh_notice(msg: str):
|
|
49
|
+
print(f"::notice::{msg}")
|
|
50
|
+
|
|
51
|
+
def load_version_from_path(path: Path):
|
|
52
|
+
content = path.read_text()
|
|
53
|
+
# Updated regex to match the new __version__ variable assignment
|
|
54
|
+
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
|
|
55
|
+
if not match:
|
|
56
|
+
# Fallback to old pattern just in case
|
|
57
|
+
match = re.search(r'def get_version\(\):\s*return\s*["\']([^"\']+)["\']', content)
|
|
58
|
+
|
|
59
|
+
if not match:
|
|
60
|
+
raise ValueError(f"Could not find version string in {path}")
|
|
61
|
+
return match.group(1)
|
|
62
|
+
|
|
63
|
+
def parse(v):
|
|
64
|
+
m = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:a(\d+))?$", v)
|
|
65
|
+
if not m:
|
|
66
|
+
raise ValueError(f"Invalid version: {v}")
|
|
67
|
+
major, minor, patch, alpha = m.groups()
|
|
68
|
+
alpha_num = int(alpha) if alpha else float('inf')
|
|
69
|
+
return (int(major), int(minor), int(patch), alpha_num)
|
|
70
|
+
|
|
71
|
+
def format_version_type(v):
|
|
72
|
+
if "a" in v:
|
|
73
|
+
return f"`{v}` (alpha)"
|
|
74
|
+
return f"`{v}` (release)"
|
|
75
|
+
|
|
76
|
+
subprocess.run(["git", "fetch", "origin"], check=True, capture_output=True)
|
|
77
|
+
|
|
78
|
+
base_ref = subprocess.check_output(
|
|
79
|
+
["git", "merge-base", "HEAD", "origin/main"],
|
|
80
|
+
text=True
|
|
81
|
+
).strip()
|
|
82
|
+
|
|
83
|
+
subprocess.run(
|
|
84
|
+
["git", "checkout", base_ref, "--", str(VERSION_FILE)],
|
|
85
|
+
check=True,
|
|
86
|
+
capture_output=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
base_version = load_version_from_path(VERSION_FILE)
|
|
90
|
+
|
|
91
|
+
subprocess.run(
|
|
92
|
+
["git", "checkout", "HEAD", "--", str(VERSION_FILE)],
|
|
93
|
+
check=True,
|
|
94
|
+
capture_output=True
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
pr_version = load_version_from_path(VERSION_FILE)
|
|
98
|
+
|
|
99
|
+
print(f"Base version: {base_version}")
|
|
100
|
+
print(f"PR version: {pr_version}")
|
|
101
|
+
|
|
102
|
+
base_parsed = parse(base_version)
|
|
103
|
+
pr_parsed = parse(pr_version)
|
|
104
|
+
|
|
105
|
+
if pr_parsed <= base_parsed:
|
|
106
|
+
gh_error(f"Version not bumped: {base_version} → {pr_version}")
|
|
107
|
+
|
|
108
|
+
write_summary("## ❌ Version Bump Check Failed\n")
|
|
109
|
+
write_summary(f"| Branch | Version |")
|
|
110
|
+
write_summary(f"|--------|---------|")
|
|
111
|
+
write_summary(f"| `main` | {format_version_type(base_version)} |")
|
|
112
|
+
write_summary(f"| PR | {format_version_type(pr_version)} |")
|
|
113
|
+
write_summary("")
|
|
114
|
+
|
|
115
|
+
if pr_parsed == base_parsed:
|
|
116
|
+
write_summary(f"**Problem:** Version unchanged at `{pr_version}`")
|
|
117
|
+
else:
|
|
118
|
+
write_summary(f"**Problem:** PR version `{pr_version}` is lower than base `{base_version}`")
|
|
119
|
+
|
|
120
|
+
write_summary("")
|
|
121
|
+
write_summary("Please update `quasarr/providers/version.py` with an incremented version.")
|
|
122
|
+
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
gh_notice(f"Version bumped: {base_version} → {pr_version}")
|
|
126
|
+
|
|
127
|
+
write_summary("## ✅ Version Bump Check Passed\n")
|
|
128
|
+
write_summary(f"| Branch | Version |")
|
|
129
|
+
write_summary(f"|--------|---------|")
|
|
130
|
+
write_summary(f"| `main` | {format_version_type(base_version)} |")
|
|
131
|
+
write_summary(f"| PR | {format_version_type(pr_version)} |")
|
|
132
|
+
EOF
|
|
133
|
+
|
|
134
|
+
- name: Comment on PR
|
|
135
|
+
if: failure()
|
|
136
|
+
env:
|
|
137
|
+
GH_TOKEN: ${{ github.token }}
|
|
138
|
+
run: gh pr comment ${{ github.event.pull_request.number }} --body-file pr_comment.md
|