redfetch 1.1.2__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {redfetch-1.1.2 → redfetch-1.3.0}/.github/workflows/pipeline.yml +57 -14
- {redfetch-1.1.2 → redfetch-1.3.0}/CHANGELOG.md +26 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/PKG-INFO +50 -15
- {redfetch-1.1.2 → redfetch-1.3.0}/README.md +35 -2
- {redfetch-1.1.2 → redfetch-1.3.0}/pyproject.toml +13 -11
- redfetch-1.3.0/src/redfetch/__about__.py +24 -0
- redfetch-1.3.0/src/redfetch/api.py +134 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/auth.py +235 -64
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/config.py +231 -82
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/config_firstrun.py +26 -12
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/desktop_shortcut.py +0 -7
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/download.py +74 -89
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/listener.py +18 -29
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/main.py +150 -57
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/meta.py +14 -11
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/navmesh.py +2 -3
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/net.py +5 -5
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/processes.py +29 -15
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/push.py +26 -31
- redfetch-1.3.0/src/redfetch/runtime_errors.py +96 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/settings.toml +15 -5
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/special.py +9 -29
- redfetch-1.3.0/src/redfetch/store.py +505 -0
- redfetch-1.3.0/src/redfetch/sync.py +261 -0
- redfetch-1.3.0/src/redfetch/sync_discovery.py +352 -0
- redfetch-1.3.0/src/redfetch/sync_executor.py +164 -0
- redfetch-1.3.0/src/redfetch/sync_planner.py +196 -0
- redfetch-1.3.0/src/redfetch/sync_remote.py +182 -0
- redfetch-1.3.0/src/redfetch/sync_types.py +348 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/terminal_ui.py +231 -160
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/terminal_ui.tcss +47 -14
- redfetch-1.3.0/src/redfetch/update_status.py +62 -0
- redfetch-1.3.0/src/redfetch/utils.py +256 -0
- redfetch-1.3.0/tests/test_breadcrumb.py +56 -0
- redfetch-1.3.0/tests/test_cancellation_e2e.py +123 -0
- redfetch-1.3.0/tests/test_check.py +144 -0
- redfetch-1.3.0/tests/test_env_selection.py +164 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_first_run_setup.py +20 -39
- redfetch-1.3.0/tests/test_licensed_resources_filtering.py +320 -0
- redfetch-1.3.0/tests/test_path_reset.py +187 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_publish_message_file_fallback.py +2 -0
- redfetch-1.3.0/tests/test_settings_local_deltas.py +56 -0
- redfetch-1.3.0/tests/test_sync_pipeline_regressions.py +530 -0
- redfetch-1.3.0/tests/test_sync_queue_rules.py +344 -0
- redfetch-1.1.2/src/redfetch/ResourceInstallBtn.js +0 -237
- redfetch-1.1.2/src/redfetch/__about__.py +0 -34
- redfetch-1.1.2/src/redfetch/api.py +0 -354
- redfetch-1.1.2/src/redfetch/models.py +0 -47
- redfetch-1.1.2/src/redfetch/store.py +0 -597
- redfetch-1.1.2/src/redfetch/sync.py +0 -447
- redfetch-1.1.2/src/redfetch/utils.py +0 -236
- redfetch-1.1.2/tests/test_cancellation_e2e.py +0 -172
- redfetch-1.1.2/tests/test_licensed_resources_filtering.py +0 -87
- redfetch-1.1.2/tests/test_path_reset.py +0 -138
- redfetch-1.1.2/tests/test_special_resources_e2e.py +0 -485
- {redfetch-1.1.2 → redfetch-1.3.0}/.gitignore +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/LICENSE +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/redfetch.ico +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/__init__.py +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/detecteq.py +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/unloadmq.py +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_is_special_or_dependency.py +0 -0
- {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_uninstall_e2e.py +0 -0
|
@@ -27,13 +27,13 @@ jobs:
|
|
|
27
27
|
package-version: ${{ steps.get_version.outputs.version }}
|
|
28
28
|
steps:
|
|
29
29
|
- name: Checkout Code
|
|
30
|
-
uses: actions/checkout@
|
|
30
|
+
uses: actions/checkout@v6
|
|
31
31
|
with:
|
|
32
32
|
fetch-depth: 0 # Ensure full Git history is available for versioning
|
|
33
33
|
fetch-tags: true
|
|
34
34
|
|
|
35
35
|
- name: Set Up Python
|
|
36
|
-
uses: actions/setup-python@
|
|
36
|
+
uses: actions/setup-python@v6
|
|
37
37
|
with:
|
|
38
38
|
python-version: '3.x'
|
|
39
39
|
|
|
@@ -85,7 +85,7 @@ jobs:
|
|
|
85
85
|
project-version: ${{ steps.set_version.outputs.version }}
|
|
86
86
|
steps:
|
|
87
87
|
- name: Checkout Code
|
|
88
|
-
uses: actions/checkout@
|
|
88
|
+
uses: actions/checkout@v6
|
|
89
89
|
with:
|
|
90
90
|
fetch-depth: 0 # Ensure full Git history is available for versioning
|
|
91
91
|
|
|
@@ -110,7 +110,7 @@ jobs:
|
|
|
110
110
|
Get-Content $env:GITHUB_ENV
|
|
111
111
|
|
|
112
112
|
- name: Set Up Python
|
|
113
|
-
uses: actions/setup-python@
|
|
113
|
+
uses: actions/setup-python@v6
|
|
114
114
|
with:
|
|
115
115
|
python-version: '3.x'
|
|
116
116
|
|
|
@@ -184,17 +184,60 @@ jobs:
|
|
|
184
184
|
--set-version-string "InternalName" "redfetch" `
|
|
185
185
|
--set-icon "redfetch.ico"
|
|
186
186
|
|
|
187
|
+
- name: Install Google Cloud KMS CNG Provider
|
|
188
|
+
shell: pwsh
|
|
189
|
+
run: |
|
|
190
|
+
if (Test-Path "$env:SystemRoot\System32\kmscng.dll") {
|
|
191
|
+
Write-Host "kmscng already installed."
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
$url = "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.3/kmscng-1.3-windows-amd64.zip"
|
|
195
|
+
$zip = Join-Path $env:RUNNER_TEMP "kmscng.zip"
|
|
196
|
+
$dest = Join-Path $env:RUNNER_TEMP "kmscng"
|
|
197
|
+
Invoke-WebRequest $url -OutFile $zip
|
|
198
|
+
Expand-Archive -Path $zip -DestinationPath $dest -Force
|
|
199
|
+
$msi = Get-ChildItem -Path $dest -Recurse -Filter *.msi | Select-Object -First 1
|
|
200
|
+
if (-not $msi) { throw "kmscng MSI not found in archive" }
|
|
201
|
+
Start-Process msiexec.exe -ArgumentList "/i `"$($msi.FullName)`" /qn /norestart" -Wait -NoNewWindow
|
|
202
|
+
if (-not (Test-Path "$env:SystemRoot\System32\kmscng.dll")) { throw "kmscng installation failed" }
|
|
203
|
+
Write-Host "kmscng installed."
|
|
204
|
+
|
|
187
205
|
- name: Sign Executable
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
206
|
+
shell: pwsh
|
|
207
|
+
env:
|
|
208
|
+
KMS_KEY: projects/code-signing-rg/locations/global/keyRings/redkeys/cryptoKeys/rgkey/cryptoKeyVersions/1
|
|
209
|
+
SIGNING_CERT: ${{ secrets.SIGNING_CERT }}
|
|
210
|
+
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
|
211
|
+
run: |
|
|
212
|
+
$CertFile = Join-Path $env:RUNNER_TEMP "user.crt"
|
|
213
|
+
$KeyFile = Join-Path $env:RUNNER_TEMP "gcp-sa.json"
|
|
214
|
+
[IO.File]::WriteAllBytes($CertFile, [Convert]::FromBase64String($env:SIGNING_CERT))
|
|
215
|
+
[IO.File]::WriteAllText($KeyFile, $env:GCP_SA_KEY)
|
|
216
|
+
$env:GOOGLE_APPLICATION_CREDENTIALS = $KeyFile
|
|
217
|
+
|
|
218
|
+
$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits" -Recurse -Filter "signtool.exe" |
|
|
219
|
+
Where-Object { $_.FullName -match "\\x64\\" } |
|
|
220
|
+
Sort-Object LastWriteTime -Descending |
|
|
221
|
+
Select-Object -First 1 -ExpandProperty FullName
|
|
222
|
+
|
|
223
|
+
if (-not $signtool) { throw "signtool.exe not found" }
|
|
224
|
+
Write-Host "Using signtool: $signtool"
|
|
225
|
+
|
|
226
|
+
$tsaServers = @('http://timestamp.digicert.com', 'http://timestamp.sectigo.com')
|
|
227
|
+
$maxAttempts = 5
|
|
228
|
+
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
|
229
|
+
$tsa = $tsaServers[($attempt - 1) % $tsaServers.Count]
|
|
230
|
+
& $signtool sign /fd SHA256 /f $CertFile /csp "Google Cloud KMS Provider" /kc $env:KMS_KEY /tr $tsa /td sha256 /v redfetch.exe
|
|
231
|
+
if ($LASTEXITCODE -eq 0) { break }
|
|
232
|
+
if ($attempt -eq $maxAttempts) { throw "Signing failed after $maxAttempts attempts" }
|
|
233
|
+
Start-Sleep -Seconds 2
|
|
234
|
+
}
|
|
235
|
+
& $signtool verify /pa /v redfetch.exe
|
|
236
|
+
|
|
237
|
+
Remove-Item -Force $CertFile, $KeyFile
|
|
195
238
|
|
|
196
239
|
- name: Upload Executable Artifact
|
|
197
|
-
uses: actions/upload-artifact@
|
|
240
|
+
uses: actions/upload-artifact@v7
|
|
198
241
|
with:
|
|
199
242
|
name: redfetch.exe
|
|
200
243
|
path: redfetch.exe
|
|
@@ -205,9 +248,9 @@ jobs:
|
|
|
205
248
|
runs-on: ubuntu-latest
|
|
206
249
|
steps:
|
|
207
250
|
- name: Checkout Code
|
|
208
|
-
uses: actions/checkout@
|
|
251
|
+
uses: actions/checkout@v6
|
|
209
252
|
- name: Download Executable Artifact
|
|
210
|
-
uses: actions/download-artifact@
|
|
253
|
+
uses: actions/download-artifact@v8
|
|
211
254
|
with:
|
|
212
255
|
name: redfetch.exe
|
|
213
256
|
- name: Redguides Publish
|
|
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-06-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `--server` on `update` and `download`: use a different server for one run.
|
|
12
|
+
- TUI: "Also start post-update" — launch EQBCS, MySEQ, or a custom program after a successful update. ([#18](https://github.com/RedGuides/redfetch/issues/18))
|
|
13
|
+
- Staff picks: Ninjadvloot.inc is now on LIVE/TEST/EMU.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Config paths that contain `eqgame.exe` are allowed with a warning instead of being blocked outright. ([#22](https://github.com/RedGuides/redfetch/issues/22))
|
|
17
|
+
- TUI: "Close MQ pre-update" and "Start MQ post-update" are now No / Ask / Yes toggles, so you can pick the prompt-each-time ("Ask") behavior from the UI instead of only on/off.
|
|
18
|
+
- Only settings you've actually changed are written to `settings.local.toml`, keeping the file tidy.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Licensed resources will now download even if you have duplicates. ([#24](https://github.com/RedGuides/redfetch/issues/24))
|
|
22
|
+
- Update checks notice when a resource's subfolder changes.
|
|
23
|
+
- Fixed "RecursionError" on startup by rolling back a dependency.
|
|
24
|
+
|
|
25
|
+
## [1.2.0] - 2026-04-05
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Custom category directories: override where `lua`, `macros`, or `plugins` install via `CATEGORY_PATHS` in `settings.local.toml`.
|
|
29
|
+
- Clearer messages for users on why certain resources are skipped or blocked.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Rewrote the sync pipeline with separate discovery, planning, and execution stages.
|
|
33
|
+
|
|
8
34
|
## [1.1.2] - 2026-02-16
|
|
9
35
|
|
|
10
36
|
### Fixed
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redfetch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Download and publish EverQuest scripts and software using the RedGuides API
|
|
5
5
|
Project-URL: Homepage, https://www.redguides.com
|
|
6
6
|
Project-URL: Documentation, https://www.redguides.com/community/resources/redfetch.3177/
|
|
7
7
|
Project-URL: Repository, https://github.com/RedGuides/redfetch
|
|
8
8
|
Project-URL: Issues, https://github.com/RedGuides/redfetch/issues
|
|
9
9
|
Project-URL: Changelog, https://github.com/RedGuides/redfetch/blob/main/CHANGELOG.md
|
|
10
|
-
Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/
|
|
10
|
+
Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/aaf2d42ab82d45abeadfafa943e2b61abba2cb60.zip
|
|
11
11
|
Author-email: Redbot <ask@redguides.com>
|
|
12
12
|
License-Expression: GPL-3.0-or-later
|
|
13
13
|
License-File: LICENSE
|
|
@@ -17,27 +17,29 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.12
|
|
20
21
|
Requires-Dist: aiofiles~=25.1
|
|
21
|
-
Requires-Dist: aiohttp~=3.
|
|
22
|
+
Requires-Dist: aiohttp~=3.14
|
|
22
23
|
Requires-Dist: aiosqlite~=0.22
|
|
23
|
-
Requires-Dist: cachetools~=7.
|
|
24
|
+
Requires-Dist: cachetools~=7.1
|
|
24
25
|
Requires-Dist: diskcache~=5.6
|
|
25
|
-
Requires-Dist: dynaconf
|
|
26
|
+
Requires-Dist: dynaconf<3.3,~=3.2
|
|
26
27
|
Requires-Dist: httpx[http2]~=0.28
|
|
27
28
|
Requires-Dist: keepachangelog~=2.0
|
|
28
|
-
Requires-Dist: keyring~=25.
|
|
29
|
+
Requires-Dist: keyring~=25.7
|
|
29
30
|
Requires-Dist: md2bbcode
|
|
30
|
-
Requires-Dist: packaging~=26.
|
|
31
|
-
Requires-Dist: platformdirs~=4.
|
|
31
|
+
Requires-Dist: packaging~=26.2
|
|
32
|
+
Requires-Dist: platformdirs~=4.10
|
|
32
33
|
Requires-Dist: psutil~=7.2
|
|
34
|
+
Requires-Dist: pydantic~=2.13
|
|
33
35
|
Requires-Dist: pyperclip~=1.11
|
|
34
36
|
Requires-Dist: pywin32; sys_platform == 'win32'
|
|
35
|
-
Requires-Dist: rich~=
|
|
37
|
+
Requires-Dist: rich~=15.0
|
|
36
38
|
Requires-Dist: tenacity~=9.1
|
|
37
|
-
Requires-Dist: textual-fspicker~=0
|
|
38
|
-
Requires-Dist: textual~=
|
|
39
|
-
Requires-Dist: tomlkit~=0.
|
|
40
|
-
Requires-Dist: typer~=0.
|
|
39
|
+
Requires-Dist: textual-fspicker~=1.0
|
|
40
|
+
Requires-Dist: textual~=8.2
|
|
41
|
+
Requires-Dist: tomlkit~=0.15
|
|
42
|
+
Requires-Dist: typer~=0.26
|
|
41
43
|
Description-Content-Type: text/markdown
|
|
42
44
|
|
|
43
45
|

|
|
@@ -108,8 +110,10 @@ To run redfetch from the command line:
|
|
|
108
110
|
> ### 📦 Resource Management
|
|
109
111
|
> - `update` - Update all watched and special resources
|
|
110
112
|
> - `--force` / `-f` - Force re-download of all watched resources
|
|
113
|
+
> - `--server` / `-s` - Switch to this server before updating (`LIVE`, `TEST`, `EMU`)
|
|
111
114
|
> - `download <ID_OR_URL>` - Download a specific resource by ID or URL
|
|
112
115
|
> - `--force` / `-f` - Force re-download by resetting this resource's download date
|
|
116
|
+
> - `--server` / `-s` - Switch to this server before downloading (`LIVE`, `TEST`, `EMU`)
|
|
113
117
|
> - `list` - List resources and dependencies in the cache database
|
|
114
118
|
> - `reset` - Reset download dates for watched resources
|
|
115
119
|
>
|
|
@@ -172,6 +176,18 @@ If there are local files you don't want overwritten by a resource, you can add t
|
|
|
172
176
|
navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
|
|
173
177
|
```
|
|
174
178
|
|
|
179
|
+
### Custom category directories
|
|
180
|
+
|
|
181
|
+
If you share `lua`, `macros`, or `plugins` directories across multiple MQ environments, you can override where an entire category is installed. Add a `CATEGORY_PATHS` section to your `settings.local.toml`:
|
|
182
|
+
|
|
183
|
+
```toml
|
|
184
|
+
[DEFAULT.CATEGORY_PATHS]
|
|
185
|
+
lua = 'D:\\shared\\lua'
|
|
186
|
+
macros = 'D:\\shared\\macros'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Absolute paths are used as-is. Relative paths are joined to `DOWNLOAD_FOLDER`. You can set this globally in `[DEFAULT]` or per-environment (`[LIVE.CATEGORY_PATHS]`, `[TEST.CATEGORY_PATHS]`, etc.).
|
|
190
|
+
|
|
175
191
|
## Tinkerers
|
|
176
192
|
|
|
177
193
|
If you self-compile MacroQuest or use a discord friend's copy, you can still keep your scripts and plugins in sync with redfetch by opting out of Very Vanilla:
|
|
@@ -184,12 +200,31 @@ redfetch.exe config SPECIAL_RESOURCES.2218.opt_in false --server TEST
|
|
|
184
200
|
|
|
185
201
|
Then assign the *Very Vanilla MQ* path to your self-compiled MacroQuest.
|
|
186
202
|
|
|
203
|
+
### Custom post-update launch
|
|
204
|
+
|
|
205
|
+
redfetch can launch extra programs after an update completes. Aside from the normal UI toggles, you can add `custom` to `POST_UPDATE_LAUNCH.targets` in `settings.local.toml`, then set `command` to whatever redfetch should run:
|
|
206
|
+
|
|
207
|
+
```toml
|
|
208
|
+
[LIVE.POST_UPDATE_LAUNCH]
|
|
209
|
+
targets = ["custom"]
|
|
210
|
+
command = ['C:\Tools\AfterRedfetch\after-update.exe', '--server', 'LIVE']
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
You can also combine it with the built-in post-update launches. For example, to start EQBCS and MySEQ and run your own Python script:
|
|
214
|
+
|
|
215
|
+
```toml
|
|
216
|
+
[LIVE.POST_UPDATE_LAUNCH]
|
|
217
|
+
targets = ["myseq", "custom", "eqbcs"]
|
|
218
|
+
command = ["C:\\Users\\Public\\Python\\python.exe", "C:\\Users\\Public\\redfetch\\after_update.py"]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
You can set these per-server, e.g. `[TEST.POST_UPDATE_LAUNCH]`, or global `[DEFAULT.POST_UPDATE_LAUNCH]`.
|
|
222
|
+
|
|
223
|
+
|
|
187
224
|
## Trailmap
|
|
188
225
|
- Add custom buttons for "fetch" tab.
|
|
189
226
|
- Option: Close after update
|
|
190
|
-
- Launch programs with cli options
|
|
191
227
|
- Indicate when updated VV is available
|
|
192
|
-
- Launch more than just mq (eqbcs, etc) upon update.
|
|
193
228
|
- Run from MQ
|
|
194
229
|
- Deeper integration with the forums
|
|
195
230
|
|
|
@@ -66,8 +66,10 @@ To run redfetch from the command line:
|
|
|
66
66
|
> ### 📦 Resource Management
|
|
67
67
|
> - `update` - Update all watched and special resources
|
|
68
68
|
> - `--force` / `-f` - Force re-download of all watched resources
|
|
69
|
+
> - `--server` / `-s` - Switch to this server before updating (`LIVE`, `TEST`, `EMU`)
|
|
69
70
|
> - `download <ID_OR_URL>` - Download a specific resource by ID or URL
|
|
70
71
|
> - `--force` / `-f` - Force re-download by resetting this resource's download date
|
|
72
|
+
> - `--server` / `-s` - Switch to this server before downloading (`LIVE`, `TEST`, `EMU`)
|
|
71
73
|
> - `list` - List resources and dependencies in the cache database
|
|
72
74
|
> - `reset` - Reset download dates for watched resources
|
|
73
75
|
>
|
|
@@ -130,6 +132,18 @@ If there are local files you don't want overwritten by a resource, you can add t
|
|
|
130
132
|
navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
|
|
131
133
|
```
|
|
132
134
|
|
|
135
|
+
### Custom category directories
|
|
136
|
+
|
|
137
|
+
If you share `lua`, `macros`, or `plugins` directories across multiple MQ environments, you can override where an entire category is installed. Add a `CATEGORY_PATHS` section to your `settings.local.toml`:
|
|
138
|
+
|
|
139
|
+
```toml
|
|
140
|
+
[DEFAULT.CATEGORY_PATHS]
|
|
141
|
+
lua = 'D:\\shared\\lua'
|
|
142
|
+
macros = 'D:\\shared\\macros'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Absolute paths are used as-is. Relative paths are joined to `DOWNLOAD_FOLDER`. You can set this globally in `[DEFAULT]` or per-environment (`[LIVE.CATEGORY_PATHS]`, `[TEST.CATEGORY_PATHS]`, etc.).
|
|
146
|
+
|
|
133
147
|
## Tinkerers
|
|
134
148
|
|
|
135
149
|
If you self-compile MacroQuest or use a discord friend's copy, you can still keep your scripts and plugins in sync with redfetch by opting out of Very Vanilla:
|
|
@@ -142,12 +156,31 @@ redfetch.exe config SPECIAL_RESOURCES.2218.opt_in false --server TEST
|
|
|
142
156
|
|
|
143
157
|
Then assign the *Very Vanilla MQ* path to your self-compiled MacroQuest.
|
|
144
158
|
|
|
159
|
+
### Custom post-update launch
|
|
160
|
+
|
|
161
|
+
redfetch can launch extra programs after an update completes. Aside from the normal UI toggles, you can add `custom` to `POST_UPDATE_LAUNCH.targets` in `settings.local.toml`, then set `command` to whatever redfetch should run:
|
|
162
|
+
|
|
163
|
+
```toml
|
|
164
|
+
[LIVE.POST_UPDATE_LAUNCH]
|
|
165
|
+
targets = ["custom"]
|
|
166
|
+
command = ['C:\Tools\AfterRedfetch\after-update.exe', '--server', 'LIVE']
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
You can also combine it with the built-in post-update launches. For example, to start EQBCS and MySEQ and run your own Python script:
|
|
170
|
+
|
|
171
|
+
```toml
|
|
172
|
+
[LIVE.POST_UPDATE_LAUNCH]
|
|
173
|
+
targets = ["myseq", "custom", "eqbcs"]
|
|
174
|
+
command = ["C:\\Users\\Public\\Python\\python.exe", "C:\\Users\\Public\\redfetch\\after_update.py"]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
You can set these per-server, e.g. `[TEST.POST_UPDATE_LAUNCH]`, or global `[DEFAULT.POST_UPDATE_LAUNCH]`.
|
|
178
|
+
|
|
179
|
+
|
|
145
180
|
## Trailmap
|
|
146
181
|
- Add custom buttons for "fetch" tab.
|
|
147
182
|
- Option: Close after update
|
|
148
|
-
- Launch programs with cli options
|
|
149
183
|
- Indicate when updated VV is available
|
|
150
|
-
- Launch more than just mq (eqbcs, etc) upon update.
|
|
151
184
|
- Run from MQ
|
|
152
185
|
- Deeper integration with the forums
|
|
153
186
|
|
|
@@ -3,6 +3,7 @@ name = "redfetch"
|
|
|
3
3
|
description = "Download and publish EverQuest scripts and software using the RedGuides API"
|
|
4
4
|
readme = "README.md"
|
|
5
5
|
license = "GPL-3.0-or-later"
|
|
6
|
+
requires-python = ">=3.12"
|
|
6
7
|
dynamic = ["version", "urls"]
|
|
7
8
|
authors = [
|
|
8
9
|
{ name = "Redbot", email = "ask@redguides.com" }
|
|
@@ -19,24 +20,25 @@ classifiers = [
|
|
|
19
20
|
dependencies = [
|
|
20
21
|
"aiofiles~=25.1",
|
|
21
22
|
"aiosqlite~=0.22",
|
|
22
|
-
"aiohttp~=3.
|
|
23
|
-
"cachetools~=7.
|
|
23
|
+
"aiohttp~=3.14",
|
|
24
|
+
"cachetools~=7.1",
|
|
24
25
|
"diskcache~=5.6",
|
|
25
|
-
"dynaconf~=3.2",
|
|
26
|
+
"dynaconf~=3.2,<3.3", # 3.3 has a bug
|
|
26
27
|
"httpx[http2]~=0.28",
|
|
27
28
|
"keepachangelog~=2.0",
|
|
28
|
-
"keyring~=25.
|
|
29
|
+
"keyring~=25.7",
|
|
29
30
|
"md2bbcode",
|
|
30
|
-
"packaging~=26.
|
|
31
|
-
"
|
|
31
|
+
"packaging~=26.2",
|
|
32
|
+
"pydantic~=2.13",
|
|
33
|
+
"platformdirs~=4.10",
|
|
32
34
|
"psutil~=7.2",
|
|
33
35
|
"pyperclip~=1.11",
|
|
34
|
-
"rich~=
|
|
36
|
+
"rich~=15.0",
|
|
35
37
|
"tenacity~=9.1",
|
|
36
|
-
"textual~=
|
|
37
|
-
"textual-fspicker~=0
|
|
38
|
-
"tomlkit~=0.
|
|
39
|
-
"typer~=0.
|
|
38
|
+
"textual~=8.2",
|
|
39
|
+
"textual-fspicker~=1.0",
|
|
40
|
+
"tomlkit~=0.15",
|
|
41
|
+
"typer~=0.26",
|
|
40
42
|
"pywin32; sys_platform == 'win32'"
|
|
41
43
|
]
|
|
42
44
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 3, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Resource API client: fetch_*() takes a client, get_*() is self-contained."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from redfetch import auth
|
|
7
|
+
from redfetch import net
|
|
8
|
+
from redfetch.sync_types import RemoteStatus, SyncModel
|
|
9
|
+
|
|
10
|
+
BASE_URL = net.BASE_URL
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceRecord(SyncModel):
|
|
14
|
+
"""Result of a single-resource API check: status plus optional payload or error."""
|
|
15
|
+
resource_id: str
|
|
16
|
+
status: RemoteStatus
|
|
17
|
+
resource: dict | None = None
|
|
18
|
+
error: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def fetch_watched_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
|
|
22
|
+
"""Fetch a single page of watched resources."""
|
|
23
|
+
url = f'{BASE_URL}/api/rgwatched'
|
|
24
|
+
try:
|
|
25
|
+
data = await net.get_json(client, url, params={'page': page})
|
|
26
|
+
last_page = data['pagination']['last_page']
|
|
27
|
+
items = data.get('resources', [])
|
|
28
|
+
return items, last_page
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"Error fetching watched resources page {page}: {e}")
|
|
31
|
+
return [], 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def fetch_licenses_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
|
|
35
|
+
"""Fetch a single page of user licenses."""
|
|
36
|
+
url = f'{BASE_URL}/api/user-licenses'
|
|
37
|
+
try:
|
|
38
|
+
data = await net.get_json(client, url, params={'page': page})
|
|
39
|
+
last_page = data['pagination']['last_page']
|
|
40
|
+
items = data.get('licenses', [])
|
|
41
|
+
return items, last_page
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Error fetching licenses page {page}: {e}")
|
|
44
|
+
return [], 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def fetch_resource_record(client: httpx.AsyncClient, resource_id: str) -> ResourceRecord:
|
|
48
|
+
"""Fetch a single resource and classify its availability."""
|
|
49
|
+
url = f'{BASE_URL}/api/resources/{resource_id}'
|
|
50
|
+
try:
|
|
51
|
+
data = await net.get_json(client, url)
|
|
52
|
+
resource = data['resource']
|
|
53
|
+
current_files = resource.get('current_files') or []
|
|
54
|
+
if not resource.get('can_download', False):
|
|
55
|
+
status: RemoteStatus = 'access_denied'
|
|
56
|
+
elif len(current_files) == 0:
|
|
57
|
+
status = 'no_files'
|
|
58
|
+
elif len(current_files) > 1:
|
|
59
|
+
status = 'multiple_files'
|
|
60
|
+
else:
|
|
61
|
+
status = 'downloadable'
|
|
62
|
+
return ResourceRecord(resource_id=resource_id, status=status, resource=resource)
|
|
63
|
+
except httpx.HTTPStatusError as e:
|
|
64
|
+
status = 'not_found' if e.response.status_code == 404 else 'fetch_error'
|
|
65
|
+
return ResourceRecord(resource_id=resource_id, status=status, error=str(e))
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return ResourceRecord(resource_id=resource_id, status='fetch_error', error=str(e))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def fetch_resource_records_batch(client: httpx.AsyncClient, resource_ids: list[str]) -> list[ResourceRecord]:
|
|
71
|
+
"""Fetch multiple resource records concurrently."""
|
|
72
|
+
if not resource_ids:
|
|
73
|
+
return []
|
|
74
|
+
return list(await asyncio.gather(
|
|
75
|
+
*(fetch_resource_record(client, rid) for rid in resource_ids)
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def get_resource_details(resource_id: int, headers: dict) -> dict:
|
|
80
|
+
"""Retrieve details of a specific resource from the API."""
|
|
81
|
+
url = f'{BASE_URL}/api/resources/{resource_id}'
|
|
82
|
+
async with httpx.AsyncClient(headers=headers, http2=True, timeout=30.0) as client:
|
|
83
|
+
response = await client.get(url)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
return response.json()['resource']
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def fetch_watched_resources(client: httpx.AsyncClient) -> list:
|
|
89
|
+
"""Fetch all watched resources with concurrent pagination."""
|
|
90
|
+
items, total_pages = await fetch_watched_page(client, 1)
|
|
91
|
+
if total_pages <= 1:
|
|
92
|
+
return items
|
|
93
|
+
|
|
94
|
+
coros = [fetch_watched_page(client, p) for p in range(2, total_pages + 1)]
|
|
95
|
+
page_results = await asyncio.gather(*coros)
|
|
96
|
+
|
|
97
|
+
for page_items, _ in page_results:
|
|
98
|
+
items.extend(page_items)
|
|
99
|
+
|
|
100
|
+
return items
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def fetch_licenses(client: httpx.AsyncClient) -> list:
|
|
104
|
+
"""Fetch all user licenses with concurrent pagination."""
|
|
105
|
+
items, total_pages = await fetch_licenses_page(client, 1)
|
|
106
|
+
if total_pages <= 1:
|
|
107
|
+
return items
|
|
108
|
+
coros = [fetch_licenses_page(client, p) for p in range(2, total_pages + 1)]
|
|
109
|
+
page_results = await asyncio.gather(*coros)
|
|
110
|
+
|
|
111
|
+
for page_items, _ in page_results:
|
|
112
|
+
items.extend(page_items)
|
|
113
|
+
|
|
114
|
+
return items
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_KISS_CACHE_TTL_SECONDS = 60
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def is_kiss_downloadable(headers, force_refresh: bool = False):
|
|
121
|
+
"""Check for level 2 access by checking KISS."""
|
|
122
|
+
cache = auth.get_disk_cache()
|
|
123
|
+
cache_key = "kiss"
|
|
124
|
+
|
|
125
|
+
if not force_refresh:
|
|
126
|
+
cached = cache.get(cache_key)
|
|
127
|
+
if cached is not None:
|
|
128
|
+
return bool(cached)
|
|
129
|
+
|
|
130
|
+
async with httpx.AsyncClient(headers=headers, http2=True) as client:
|
|
131
|
+
record = await fetch_resource_record(client, "4")
|
|
132
|
+
result = record.status == "downloadable"
|
|
133
|
+
cache.set(cache_key, bool(result), expire=_KISS_CACHE_TTL_SECONDS)
|
|
134
|
+
return result
|