quasarr 2.4.2__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.

Files changed (104) hide show
  1. quasarr-2.4.5/.github/Changelog.md +5 -0
  2. quasarr-2.4.5/.github/FUNDING.yml +1 -0
  3. quasarr-2.4.5/.github/ISSUE_TEMPLATE/bug_report.yml +67 -0
  4. quasarr-2.4.5/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. quasarr-2.4.5/.github/workflows/Beta.yml +191 -0
  6. quasarr-2.4.5/.github/workflows/HostnameRedaction.yml +256 -0
  7. quasarr-2.4.5/.github/workflows/PrVersionBumpCheck.yml +138 -0
  8. quasarr-2.4.5/.github/workflows/Release.yml +272 -0
  9. quasarr-2.4.5/.gitignore +17 -0
  10. {quasarr-2.4.2 → quasarr-2.4.5}/PKG-INFO +11 -20
  11. quasarr-2.4.5/Quasarr.png +0 -0
  12. quasarr-2.4.5/Quasarr.py +11 -0
  13. {quasarr-2.4.2 → quasarr-2.4.5}/README.md +5 -5
  14. quasarr-2.4.5/docker/Dockerfile +34 -0
  15. quasarr-2.4.5/docker/dev-services-compose.yml +98 -0
  16. quasarr-2.4.5/docker/dev-setup.md +21 -0
  17. quasarr-2.4.5/docker/docker-compose.yml +19 -0
  18. quasarr-2.4.5/pyproject.toml +40 -0
  19. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/__init__.py +2 -2
  20. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/captcha/__init__.py +6 -6
  21. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/packages/__init__.py +2 -1
  22. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/html_images.py +1 -1
  23. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/imdb_metadata.py +1 -1
  24. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/sessions/dd.py +1 -1
  25. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/statistics.py +8 -8
  26. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/version.py +12 -4
  27. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/he.py +2 -2
  28. quasarr-2.4.2/quasarr.egg-info/PKG-INFO +0 -338
  29. quasarr-2.4.2/quasarr.egg-info/SOURCES.txt +0 -86
  30. quasarr-2.4.2/quasarr.egg-info/dependency_links.txt +0 -1
  31. quasarr-2.4.2/quasarr.egg-info/entry_points.txt +0 -2
  32. quasarr-2.4.2/quasarr.egg-info/not-zip-safe +0 -1
  33. quasarr-2.4.2/quasarr.egg-info/requires.txt +0 -6
  34. quasarr-2.4.2/quasarr.egg-info/top_level.txt +0 -1
  35. quasarr-2.4.2/setup.cfg +0 -4
  36. quasarr-2.4.2/setup.py +0 -43
  37. {quasarr-2.4.2 → quasarr-2.4.5}/LICENSE +0 -0
  38. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/__init__.py +0 -0
  39. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/arr/__init__.py +0 -0
  40. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/config/__init__.py +0 -0
  41. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/packages/__init__.py +0 -0
  42. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/sponsors_helper/__init__.py +0 -0
  43. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/api/statistics/__init__.py +0 -0
  44. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/__init__.py +0 -0
  45. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/__init__.py +0 -0
  46. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/al.py +0 -0
  47. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
  48. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/linkcrypters/hide.py +0 -0
  49. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/__init__.py +0 -0
  50. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/al.py +0 -0
  51. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/by.py +0 -0
  52. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/dd.py +0 -0
  53. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/dj.py +0 -0
  54. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/dl.py +0 -0
  55. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/dt.py +0 -0
  56. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/dw.py +0 -0
  57. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/he.py +0 -0
  58. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/mb.py +0 -0
  59. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/nk.py +0 -0
  60. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/nx.py +0 -0
  61. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/sf.py +0 -0
  62. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/sj.py +0 -0
  63. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/sl.py +0 -0
  64. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/wd.py +0 -0
  65. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/downloads/sources/wx.py +0 -0
  66. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/__init__.py +0 -0
  67. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/auth.py +0 -0
  68. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/cloudflare.py +0 -0
  69. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/hostname_issues.py +0 -0
  70. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/html_templates.py +0 -0
  71. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/jd_cache.py +0 -0
  72. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/log.py +0 -0
  73. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/myjd_api.py +0 -0
  74. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/notifications.py +0 -0
  75. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/obfuscated.py +0 -0
  76. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/sessions/__init__.py +0 -0
  77. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/sessions/al.py +0 -0
  78. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/sessions/dl.py +0 -0
  79. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/sessions/nx.py +0 -0
  80. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/shared_state.py +0 -0
  81. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/utils.py +0 -0
  82. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/providers/web_server.py +0 -0
  83. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/__init__.py +0 -0
  84. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/__init__.py +0 -0
  85. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/al.py +0 -0
  86. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/by.py +0 -0
  87. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/dd.py +0 -0
  88. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/dj.py +0 -0
  89. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/dl.py +0 -0
  90. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/dt.py +0 -0
  91. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/dw.py +0 -0
  92. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/fx.py +0 -0
  93. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/mb.py +0 -0
  94. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/nk.py +0 -0
  95. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/nx.py +0 -0
  96. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/sf.py +0 -0
  97. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/sj.py +0 -0
  98. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/sl.py +0 -0
  99. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/wd.py +0 -0
  100. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/search/sources/wx.py +0 -0
  101. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/storage/__init__.py +0 -0
  102. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/storage/config.py +0 -0
  103. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/storage/setup.py +0 -0
  104. {quasarr-2.4.2 → quasarr-2.4.5}/quasarr/storage/sqlite_database.py +0 -0
@@ -0,0 +1,5 @@
1
+ ### Install / Update:
2
+
3
+ `uv tool upgrade quasarr`
4
+
5
+ ### Changelog:
@@ -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,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Chat with the Community / Propose new ideas
4
+ url: https://discord.gg/eM4zA2wWQb
5
+ about: Do not use for issue reports
@@ -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