pix-cli 0.1.0__tar.gz → 0.1.2__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.
- pix_cli-0.1.2/LICENSE +21 -0
- pix_cli-0.1.2/PKG-INFO +250 -0
- pix_cli-0.1.2/README.md +222 -0
- pix_cli-0.1.2/README.zh-CN.md +220 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/docs/architecture.md +5 -4
- {pix_cli-0.1.0 → pix_cli-0.1.2}/docs/decisions.md +2 -2
- pix_cli-0.1.2/pyproject.toml +58 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/config.py +13 -25
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/logger.py +19 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/shrink.py +4 -1
- {pix_cli-0.1.0 → pix_cli-0.1.2}/tests/test_cli.py +31 -3
- {pix_cli-0.1.0 → pix_cli-0.1.2}/uv.lock +1 -1
- pix_cli-0.1.0/PKG-INFO +0 -40
- pix_cli-0.1.0/README.md +0 -31
- pix_cli-0.1.0/pyproject.toml +0 -30
- {pix_cli-0.1.0 → pix_cli-0.1.2}/.github/workflows/ci.yml +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/.github/workflows/publish.yml +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/.gitignore +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/.python-version +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/AGENTS.md +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/docs/development.md +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/docs/roadmap.md +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/__init__.py +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/cli.py +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/default_config.toml +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/doctor.py +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/py.typed +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/rclone.py +0 -0
- {pix_cli-0.1.0 → pix_cli-0.1.2}/src/pix/sync.py +0 -0
pix_cli-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 uzvg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pix_cli-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pix-cli
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Batch AVIF shrink and image-host sync CLI
|
|
5
|
+
Project-URL: Homepage, https://github.com/uzvg/pix
|
|
6
|
+
Project-URL: Repository, https://github.com/uzvg/pix
|
|
7
|
+
Project-URL: Issues, https://github.com/uzvg/pix/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/uzvg/pix/releases
|
|
9
|
+
Author-email: uzvg <uzvg@qq.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: avif,cli,image-compression,image-host,rclone
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
24
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: pyvips[binary]>=3.2
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pix
|
|
30
|
+
|
|
31
|
+
**Batch AVIF shrink and image-host sync CLI.**
|
|
32
|
+
|
|
33
|
+
`pix` incrementally compresses local image directories to AVIF, mirrors originals to cloud storage, and publishes compressed images to an image host. Compression uses pyvips; sync is delegated to rclone.
|
|
34
|
+
|
|
35
|
+
[中文](https://github.com/uzvg/pix/blob/main/README.zh-CN.md) · [Architecture](https://github.com/uzvg/pix/blob/main/docs/architecture.md) · [Development](https://github.com/uzvg/pix/blob/main/docs/development.md) · [Decisions](https://github.com/uzvg/pix/blob/main/docs/decisions.md) · [Roadmap](https://github.com/uzvg/pix/blob/main/docs/roadmap.md)
|
|
36
|
+
|
|
37
|
+
> Status: core commands are implemented and tested. Real R2 and Windows smoke tests are still pending; see the [roadmap](https://github.com/uzvg/pix/blob/main/docs/roadmap.md).
|
|
38
|
+
|
|
39
|
+
## What pix does
|
|
40
|
+
|
|
41
|
+
| Command | Purpose |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
|
|
44
|
+
| `pix backup` | Bidirectionally mirror an original photo directory to a cloud drive |
|
|
45
|
+
| `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
|
|
46
|
+
| `pix doctor` | Check Python, libvips, rclone and configuration |
|
|
47
|
+
| `pix config init` | Write a commented default configuration file |
|
|
48
|
+
|
|
49
|
+
`pix` is deliberately narrow:
|
|
50
|
+
|
|
51
|
+
- It is not an rclone replacement and does not manage credentials.
|
|
52
|
+
- It does not read repository config or care about version control.
|
|
53
|
+
- It has no one-shot `pix run`; you chain the commands yourself.
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
59
|
+
│
|
|
60
|
+
└── publish (rclone bisync) ──> r2:images
|
|
61
|
+
photos/ ── backup (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Command | What actually runs | Who does the work |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `shrink` | pyvips thumbnail + AVIF encode | pix |
|
|
67
|
+
| `backup` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
|
|
68
|
+
| `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
|
|
69
|
+
| `doctor` / `config init` | Dependency checks / config scaffold | pix |
|
|
70
|
+
|
|
71
|
+
**Sync is rclone.** `backup` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
|
|
72
|
+
|
|
73
|
+
`backup` / `publish` are **bidirectional mirrors**, not one-way backups. Deletions propagate in both directions.
|
|
74
|
+
|
|
75
|
+
## Requirements
|
|
76
|
+
|
|
77
|
+
- Python 3.11+
|
|
78
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
79
|
+
- rclone >= 1.66 for `backup` / `publish`
|
|
80
|
+
- At least one remote already configured in rclone
|
|
81
|
+
|
|
82
|
+
`pyvips[binary]` bundles libvips, so no separate vips installation is required.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
Not yet published on PyPI; install from source:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git clone https://github.com/uzvg/pix.git
|
|
90
|
+
cd pix
|
|
91
|
+
uv tool install --editable .
|
|
92
|
+
pix --version
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`. An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`, or remove it with `uv tool uninstall pix-cli`.
|
|
96
|
+
|
|
97
|
+
## Configure rclone
|
|
98
|
+
|
|
99
|
+
Only needed for `backup` / `publish`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
rclone config # create a remote, for example onedrive or r2
|
|
103
|
+
rclone lsd onedrive: # verify that it works
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
rclone stores credentials in `~/.config/rclone/rclone.conf` (Windows: `%APPDATA%\rclone\rclone.conf`). `pix` passes the remote name through and does not store or validate credentials.
|
|
107
|
+
|
|
108
|
+
## Configure pix (optional)
|
|
109
|
+
|
|
110
|
+
Default configuration paths:
|
|
111
|
+
|
|
112
|
+
- Linux/macOS: `$XDG_CONFIG_HOME/pix/config.toml`, or `~/.config/pix/config.toml`
|
|
113
|
+
- Windows: `%APPDATA%\pix\config.toml`
|
|
114
|
+
|
|
115
|
+
If the file is missing, built-in defaults are used. Generate a commented file with:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pix config init
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file.
|
|
122
|
+
|
|
123
|
+
Common keys:
|
|
124
|
+
|
|
125
|
+
| Section | Key | Default | Meaning |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `[shrink]` | `width` | `1920` | Longest edge in pixels |
|
|
128
|
+
| | `quality` | `85` | AVIF quality, 1–100 |
|
|
129
|
+
| | `effort` | `5` | libvips effort, 0–9 |
|
|
130
|
+
| | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
|
|
131
|
+
| | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
|
|
132
|
+
| `[backup]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
|
|
133
|
+
| | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
|
|
134
|
+
| `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
|
|
135
|
+
| | `urls` | `false` | Emit a JSON URL manifest |
|
|
136
|
+
| | `exclude` | `[]` | Same as backup |
|
|
137
|
+
| | `max_delete` | `10` | Same as backup |
|
|
138
|
+
| `[rclone]` | `binary` | `rclone` | rclone executable name or path |
|
|
139
|
+
|
|
140
|
+
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; silence is `--quiet` (no short form).
|
|
141
|
+
|
|
142
|
+
## Quick start
|
|
143
|
+
|
|
144
|
+
1. Check the environment:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pix doctor
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
2. Compress images:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif --dry-run
|
|
154
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan.
|
|
158
|
+
|
|
159
|
+
3. Back up originals:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pix backup -s ~/Pictures -d onedrive:photos --dry-run
|
|
163
|
+
pix backup -s ~/Pictures -d onedrive:photos --resync
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`--resync` is required on the first run. Always preview with `--dry-run` first.
|
|
167
|
+
|
|
168
|
+
4. Publish the AVIF output:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pix publish -s ~/Pictures-avif -d r2:images \
|
|
172
|
+
--base-url https://img.example.com --urls > urls.json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`publish` requires at least one `.avif` in the source and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync.
|
|
176
|
+
|
|
177
|
+
5. Re-run later:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
181
|
+
pix backup -s ~/Pictures -d onedrive:photos
|
|
182
|
+
pix publish -s ~/Pictures-avif -d r2:images --urls
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Only changes are encoded and transferred.
|
|
186
|
+
|
|
187
|
+
## Command cheat sheet
|
|
188
|
+
|
|
189
|
+
| Task | Command |
|
|
190
|
+
|---|---|
|
|
191
|
+
| Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
|
|
192
|
+
| Re-encode everything | `pix shrink -s SRC -d OUT --force` |
|
|
193
|
+
| Remove outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
|
|
194
|
+
| Preview a sync | `pix backup -s SRC -d REMOTE --dry-run` |
|
|
195
|
+
| First run, or after changing excludes | `pix backup -s SRC -d REMOTE --resync` |
|
|
196
|
+
| Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
197
|
+
| Diagnose the environment | `pix doctor` |
|
|
198
|
+
| Generate a config file | `pix config init` |
|
|
199
|
+
|
|
200
|
+
## Safety rules
|
|
201
|
+
|
|
202
|
+
| Situation | What happens | Protection |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
|
|
205
|
+
| First sync | rclone has no baseline and errors | Add `--resync` explicitly |
|
|
206
|
+
| Excludes changed | The old baseline may treat excluded files as deletions | Run with `--resync` again after changing excludes |
|
|
207
|
+
| Empty source | `backup` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
|
|
208
|
+
| `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
|
|
209
|
+
| Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
|
|
210
|
+
| State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
|
|
211
|
+
|
|
212
|
+
## Output and exit codes
|
|
213
|
+
|
|
214
|
+
- Logs and progress go to stderr.
|
|
215
|
+
- stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
|
|
216
|
+
- Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
|
|
217
|
+
- `backup` / `publish` return rclone's exit code unchanged.
|
|
218
|
+
|
|
219
|
+
URL manifest example:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"2024/a.avif": "https://img.example.com/2024/a.avif"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Troubleshooting
|
|
228
|
+
|
|
229
|
+
| Symptom | Fix |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `pyvips/libvips unavailable` | Reinstall to bring libvips: `uv tool install --force --editable /path/to/pix`; or install libvips manually (Linux: `apt install libvips42`, Windows: libvips release zip on `PATH`) |
|
|
232
|
+
| rclone is older than 1.66 | Upgrade rclone |
|
|
233
|
+
| bisync reports a missing baseline | Add `--resync` on the first run |
|
|
234
|
+
| `OUT/.pix/lock` already exists | Check for another `shrink`, then remove a stale lock |
|
|
235
|
+
| Configuration fails to parse | Run `pix doctor` to locate the file; fix the TOML or remove the file to fall back to defaults |
|
|
236
|
+
| `publish` says the source has no `.avif` | Run `pix shrink` first |
|
|
237
|
+
| URL-related error | `base_url` must start with `http://` or `https://`, and `--urls` requires it |
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv sync
|
|
243
|
+
uv run pytest
|
|
244
|
+
uv run pix --version
|
|
245
|
+
uv build
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The code must stay compatible with Python 3.11. See [development.md](https://github.com/uzvg/pix/blob/main/docs/development.md) for tests, packaging and release details. Behavior is specified in [architecture.md](https://github.com/uzvg/pix/blob/main/docs/architecture.md); rationale lives in [decisions.md](https://github.com/uzvg/pix/blob/main/docs/decisions.md).
|
|
249
|
+
|
|
250
|
+
The detailed docs are currently written in Chinese.
|
pix_cli-0.1.2/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# pix
|
|
2
|
+
|
|
3
|
+
**Batch AVIF shrink and image-host sync CLI.**
|
|
4
|
+
|
|
5
|
+
`pix` incrementally compresses local image directories to AVIF, mirrors originals to cloud storage, and publishes compressed images to an image host. Compression uses pyvips; sync is delegated to rclone.
|
|
6
|
+
|
|
7
|
+
[中文](https://github.com/uzvg/pix/blob/main/README.zh-CN.md) · [Architecture](https://github.com/uzvg/pix/blob/main/docs/architecture.md) · [Development](https://github.com/uzvg/pix/blob/main/docs/development.md) · [Decisions](https://github.com/uzvg/pix/blob/main/docs/decisions.md) · [Roadmap](https://github.com/uzvg/pix/blob/main/docs/roadmap.md)
|
|
8
|
+
|
|
9
|
+
> Status: core commands are implemented and tested. Real R2 and Windows smoke tests are still pending; see the [roadmap](https://github.com/uzvg/pix/blob/main/docs/roadmap.md).
|
|
10
|
+
|
|
11
|
+
## What pix does
|
|
12
|
+
|
|
13
|
+
| Command | Purpose |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
|
|
16
|
+
| `pix backup` | Bidirectionally mirror an original photo directory to a cloud drive |
|
|
17
|
+
| `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
|
|
18
|
+
| `pix doctor` | Check Python, libvips, rclone and configuration |
|
|
19
|
+
| `pix config init` | Write a commented default configuration file |
|
|
20
|
+
|
|
21
|
+
`pix` is deliberately narrow:
|
|
22
|
+
|
|
23
|
+
- It is not an rclone replacement and does not manage credentials.
|
|
24
|
+
- It does not read repository config or care about version control.
|
|
25
|
+
- It has no one-shot `pix run`; you chain the commands yourself.
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
31
|
+
│
|
|
32
|
+
└── publish (rclone bisync) ──> r2:images
|
|
33
|
+
photos/ ── backup (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Command | What actually runs | Who does the work |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `shrink` | pyvips thumbnail + AVIF encode | pix |
|
|
39
|
+
| `backup` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
|
|
40
|
+
| `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
|
|
41
|
+
| `doctor` / `config init` | Dependency checks / config scaffold | pix |
|
|
42
|
+
|
|
43
|
+
**Sync is rclone.** `backup` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
|
|
44
|
+
|
|
45
|
+
`backup` / `publish` are **bidirectional mirrors**, not one-way backups. Deletions propagate in both directions.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python 3.11+
|
|
50
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
51
|
+
- rclone >= 1.66 for `backup` / `publish`
|
|
52
|
+
- At least one remote already configured in rclone
|
|
53
|
+
|
|
54
|
+
`pyvips[binary]` bundles libvips, so no separate vips installation is required.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
Not yet published on PyPI; install from source:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/uzvg/pix.git
|
|
62
|
+
cd pix
|
|
63
|
+
uv tool install --editable .
|
|
64
|
+
pix --version
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`. An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`, or remove it with `uv tool uninstall pix-cli`.
|
|
68
|
+
|
|
69
|
+
## Configure rclone
|
|
70
|
+
|
|
71
|
+
Only needed for `backup` / `publish`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
rclone config # create a remote, for example onedrive or r2
|
|
75
|
+
rclone lsd onedrive: # verify that it works
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
rclone stores credentials in `~/.config/rclone/rclone.conf` (Windows: `%APPDATA%\rclone\rclone.conf`). `pix` passes the remote name through and does not store or validate credentials.
|
|
79
|
+
|
|
80
|
+
## Configure pix (optional)
|
|
81
|
+
|
|
82
|
+
Default configuration paths:
|
|
83
|
+
|
|
84
|
+
- Linux/macOS: `$XDG_CONFIG_HOME/pix/config.toml`, or `~/.config/pix/config.toml`
|
|
85
|
+
- Windows: `%APPDATA%\pix\config.toml`
|
|
86
|
+
|
|
87
|
+
If the file is missing, built-in defaults are used. Generate a commented file with:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pix config init
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file.
|
|
94
|
+
|
|
95
|
+
Common keys:
|
|
96
|
+
|
|
97
|
+
| Section | Key | Default | Meaning |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `[shrink]` | `width` | `1920` | Longest edge in pixels |
|
|
100
|
+
| | `quality` | `85` | AVIF quality, 1–100 |
|
|
101
|
+
| | `effort` | `5` | libvips effort, 0–9 |
|
|
102
|
+
| | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
|
|
103
|
+
| | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
|
|
104
|
+
| `[backup]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
|
|
105
|
+
| | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
|
|
106
|
+
| `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
|
|
107
|
+
| | `urls` | `false` | Emit a JSON URL manifest |
|
|
108
|
+
| | `exclude` | `[]` | Same as backup |
|
|
109
|
+
| | `max_delete` | `10` | Same as backup |
|
|
110
|
+
| `[rclone]` | `binary` | `rclone` | rclone executable name or path |
|
|
111
|
+
|
|
112
|
+
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; silence is `--quiet` (no short form).
|
|
113
|
+
|
|
114
|
+
## Quick start
|
|
115
|
+
|
|
116
|
+
1. Check the environment:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pix doctor
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
2. Compress images:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif --dry-run
|
|
126
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan.
|
|
130
|
+
|
|
131
|
+
3. Back up originals:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pix backup -s ~/Pictures -d onedrive:photos --dry-run
|
|
135
|
+
pix backup -s ~/Pictures -d onedrive:photos --resync
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`--resync` is required on the first run. Always preview with `--dry-run` first.
|
|
139
|
+
|
|
140
|
+
4. Publish the AVIF output:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pix publish -s ~/Pictures-avif -d r2:images \
|
|
144
|
+
--base-url https://img.example.com --urls > urls.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`publish` requires at least one `.avif` in the source and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync.
|
|
148
|
+
|
|
149
|
+
5. Re-run later:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
153
|
+
pix backup -s ~/Pictures -d onedrive:photos
|
|
154
|
+
pix publish -s ~/Pictures-avif -d r2:images --urls
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Only changes are encoded and transferred.
|
|
158
|
+
|
|
159
|
+
## Command cheat sheet
|
|
160
|
+
|
|
161
|
+
| Task | Command |
|
|
162
|
+
|---|---|
|
|
163
|
+
| Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
|
|
164
|
+
| Re-encode everything | `pix shrink -s SRC -d OUT --force` |
|
|
165
|
+
| Remove outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
|
|
166
|
+
| Preview a sync | `pix backup -s SRC -d REMOTE --dry-run` |
|
|
167
|
+
| First run, or after changing excludes | `pix backup -s SRC -d REMOTE --resync` |
|
|
168
|
+
| Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
169
|
+
| Diagnose the environment | `pix doctor` |
|
|
170
|
+
| Generate a config file | `pix config init` |
|
|
171
|
+
|
|
172
|
+
## Safety rules
|
|
173
|
+
|
|
174
|
+
| Situation | What happens | Protection |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
|
|
177
|
+
| First sync | rclone has no baseline and errors | Add `--resync` explicitly |
|
|
178
|
+
| Excludes changed | The old baseline may treat excluded files as deletions | Run with `--resync` again after changing excludes |
|
|
179
|
+
| Empty source | `backup` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
|
|
180
|
+
| `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
|
|
181
|
+
| Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
|
|
182
|
+
| State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
|
|
183
|
+
|
|
184
|
+
## Output and exit codes
|
|
185
|
+
|
|
186
|
+
- Logs and progress go to stderr.
|
|
187
|
+
- stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
|
|
188
|
+
- Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
|
|
189
|
+
- `backup` / `publish` return rclone's exit code unchanged.
|
|
190
|
+
|
|
191
|
+
URL manifest example:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"2024/a.avif": "https://img.example.com/2024/a.avif"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Troubleshooting
|
|
200
|
+
|
|
201
|
+
| Symptom | Fix |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `pyvips/libvips unavailable` | Reinstall to bring libvips: `uv tool install --force --editable /path/to/pix`; or install libvips manually (Linux: `apt install libvips42`, Windows: libvips release zip on `PATH`) |
|
|
204
|
+
| rclone is older than 1.66 | Upgrade rclone |
|
|
205
|
+
| bisync reports a missing baseline | Add `--resync` on the first run |
|
|
206
|
+
| `OUT/.pix/lock` already exists | Check for another `shrink`, then remove a stale lock |
|
|
207
|
+
| Configuration fails to parse | Run `pix doctor` to locate the file; fix the TOML or remove the file to fall back to defaults |
|
|
208
|
+
| `publish` says the source has no `.avif` | Run `pix shrink` first |
|
|
209
|
+
| URL-related error | `base_url` must start with `http://` or `https://`, and `--urls` requires it |
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
uv sync
|
|
215
|
+
uv run pytest
|
|
216
|
+
uv run pix --version
|
|
217
|
+
uv build
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The code must stay compatible with Python 3.11. See [development.md](https://github.com/uzvg/pix/blob/main/docs/development.md) for tests, packaging and release details. Behavior is specified in [architecture.md](https://github.com/uzvg/pix/blob/main/docs/architecture.md); rationale lives in [decisions.md](https://github.com/uzvg/pix/blob/main/docs/decisions.md).
|
|
221
|
+
|
|
222
|
+
The detailed docs are currently written in Chinese.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# pix
|
|
2
|
+
|
|
3
|
+
**批量 AVIF 压缩与图床同步 CLI。**
|
|
4
|
+
|
|
5
|
+
`pix` 把本地图片目录增量压缩为 AVIF,把原图双向镜像到云盘,并把压缩结果发布到图床。压缩使用 pyvips,同步交给 rclone。
|
|
6
|
+
|
|
7
|
+
[English](https://github.com/uzvg/pix/blob/main/README.md) · [行为规格](https://github.com/uzvg/pix/blob/main/docs/architecture.md) · [开发指南](https://github.com/uzvg/pix/blob/main/docs/development.md) · [技术决策](https://github.com/uzvg/pix/blob/main/docs/decisions.md) · [路线图](https://github.com/uzvg/pix/blob/main/docs/roadmap.md)
|
|
8
|
+
|
|
9
|
+
> 状态:核心命令已实现并通过测试;真实 R2 与 Windows 冒烟测试仍待完成,见[路线图](https://github.com/uzvg/pix/blob/main/docs/roadmap.md)。
|
|
10
|
+
|
|
11
|
+
## pix 做什么
|
|
12
|
+
|
|
13
|
+
| 命令 | 作用 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `pix shrink` | 递归压缩图片为 AVIF,只重新编码变化的文件 |
|
|
16
|
+
| `pix backup` | 把原图目录与云盘做双向镜像 |
|
|
17
|
+
| `pix publish` | 把 AVIF 输出目录与图床做双向镜像,可选输出公开 URL |
|
|
18
|
+
| `pix doctor` | 检查 Python、libvips、rclone 与配置 |
|
|
19
|
+
| `pix config init` | 生成带注释的默认配置文件 |
|
|
20
|
+
|
|
21
|
+
`pix` 刻意保持边界清晰:
|
|
22
|
+
|
|
23
|
+
- 不是 rclone 替代品,不管理云盘凭据。
|
|
24
|
+
- 不读取仓库配置,不感知版本控制状态。
|
|
25
|
+
- 没有一键 `pix run`,命令由你自行组合。
|
|
26
|
+
|
|
27
|
+
## 工作原理
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
31
|
+
│
|
|
32
|
+
└── publish (rclone bisync) ──> r2:images
|
|
33
|
+
photos/ ── backup (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| 命令 | 实际执行的内容 | 谁负责 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `shrink` | pyvips 缩放并编码 AVIF | pix |
|
|
39
|
+
| `backup` | `rclone bisync SRC REMOTE` | pix 拼参数,rclone 执行 |
|
|
40
|
+
| `publish` | `rclone bisync OUT REMOTE`,排除 `.pix/**` | pix + rclone |
|
|
41
|
+
| `doctor` / `config init` | 依赖检查 / 配置脚手架 | pix |
|
|
42
|
+
|
|
43
|
+
**同步由 rclone 执行。** `backup` / `publish` 只负责拼装 `rclone bisync` 参数;认证、传输和冲突处理都由 rclone 完成。使用前请先用 `rclone config` 配好 remote。`pix` 不读取也不写入 `rclone.conf`。
|
|
44
|
+
|
|
45
|
+
`backup` / `publish` 是**双向镜像**,不是单向备份。删除会双向传播。
|
|
46
|
+
|
|
47
|
+
## 前置条件
|
|
48
|
+
|
|
49
|
+
- Python 3.11+
|
|
50
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
51
|
+
- `backup` / `publish` 需要 rclone >= 1.66
|
|
52
|
+
- 已在 rclone 中配置好至少一个 remote
|
|
53
|
+
|
|
54
|
+
`pyvips[binary]` 自带 libvips,不需要单独安装 vips。
|
|
55
|
+
|
|
56
|
+
## 安装
|
|
57
|
+
|
|
58
|
+
尚未发布到 PyPI,从源码安装:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/uzvg/pix.git
|
|
62
|
+
cd pix
|
|
63
|
+
uv tool install --editable .
|
|
64
|
+
pix --version
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Linux 安装到 `~/.local/bin/pix`,Windows 为 `%USERPROFILE%\.local\bin\pix.exe`。editable 安装修改源码后立即生效。重装用 `uv tool install --force --editable .`,卸载用 `uv tool uninstall pix-cli`。
|
|
68
|
+
|
|
69
|
+
## 配置 rclone
|
|
70
|
+
|
|
71
|
+
仅 `backup` / `publish` 需要:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
rclone config # 新建 remote,例如 onedrive 或 r2
|
|
75
|
+
rclone lsd onedrive: # 验证是否可用
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
rclone 把凭据保存在 `~/.config/rclone/rclone.conf`(Windows:`%APPDATA%\rclone\rclone.conf`)。`pix` 只把 remote 名称透传给 rclone,不保存也不校验凭据。
|
|
79
|
+
|
|
80
|
+
## 配置 pix(可选)
|
|
81
|
+
|
|
82
|
+
默认配置路径:
|
|
83
|
+
|
|
84
|
+
- Linux/macOS:`$XDG_CONFIG_HOME/pix/config.toml`,未设置时为 `~/.config/pix/config.toml`
|
|
85
|
+
- Windows:`%APPDATA%\pix\config.toml`
|
|
86
|
+
|
|
87
|
+
文件不存在时使用内置默认值。生成带注释的配置文件:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pix config init
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
已有文件不会被覆盖;需要替换时加 `--force`。用 `--config PATH`(写在子命令之前)指向其他文件。
|
|
94
|
+
|
|
95
|
+
常用配置项:
|
|
96
|
+
|
|
97
|
+
| 段 | 键 | 默认值 | 说明 |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `[shrink]` | `width` | `1920` | 最长边像素 |
|
|
100
|
+
| | `quality` | `85` | AVIF 质量,1–100 |
|
|
101
|
+
| | `effort` | `5` | libvips effort,0–9 |
|
|
102
|
+
| | `min_size` | `0` | 小于该值的源文件跳过;支持 `500K`、`1M`、`2G` |
|
|
103
|
+
| | `include` | `jpg`、`jpeg`、`png`、`webp` | 扫描扩展名,不带点 |
|
|
104
|
+
| `[backup]` | `exclude` | `[]` | rclone 排除 glob;命令行项会追加 |
|
|
105
|
+
| | `max_delete` | `10` | 删除百分比上限;`-1` 不限制,`0` 禁止删除 |
|
|
106
|
+
| `[publish]` | `base_url` | `""` | 公开 URL 前缀,必须以 `http://` 或 `https://` 开头 |
|
|
107
|
+
| | `urls` | `false` | 是否输出 JSON URL 清单 |
|
|
108
|
+
| | `exclude` | `[]` | 同 `backup` |
|
|
109
|
+
| | `max_delete` | `10` | 同 `backup` |
|
|
110
|
+
| `[rclone]` | `binary` | `rclone` | rclone 可执行文件名或路径 |
|
|
111
|
+
|
|
112
|
+
优先级为**命令行 > pix 配置 > 内置默认**。`--exclude` 是追加而非覆盖配置项。`-q` 表示 quality,静默模式用 `--quiet`(无短选项)。
|
|
113
|
+
|
|
114
|
+
## 快速上手
|
|
115
|
+
|
|
116
|
+
1. 检查环境:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pix doctor
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
2. 压缩图片:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif --dry-run
|
|
126
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
首次全量编码,之后跳过未变化的文件。状态保存在 `OUT/.pix/state.json`。修改 `width`、`quality` 或 `effort` 会触发全量重编码。`--dry-run` 只打印计划。
|
|
130
|
+
|
|
131
|
+
3. 备份原图:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pix backup -s ~/Pictures -d onedrive:photos --dry-run
|
|
135
|
+
pix backup -s ~/Pictures -d onedrive:photos --resync
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
首次运行必须显式加 `--resync`。建议先跑 `--dry-run` 预览。
|
|
139
|
+
|
|
140
|
+
4. 发布 AVIF 输出:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pix publish -s ~/Pictures-avif -d r2:images \
|
|
144
|
+
--base-url https://img.example.com --urls > urls.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`publish` 要求源目录至少有一个 `.avif`,并固定排除 `.pix/**`。URL 清单只在同步成功且不是 `--dry-run` 时输出到 stdout。
|
|
148
|
+
|
|
149
|
+
5. 之后重复运行:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
153
|
+
pix backup -s ~/Pictures -d onedrive:photos
|
|
154
|
+
pix publish -s ~/Pictures-avif -d r2:images --urls
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
只处理变化的部分。
|
|
158
|
+
|
|
159
|
+
## 常用命令
|
|
160
|
+
|
|
161
|
+
| 场景 | 命令 |
|
|
162
|
+
|---|---|
|
|
163
|
+
| 预览压缩 | `pix shrink -s SRC -d OUT --dry-run` |
|
|
164
|
+
| 强制全部重压 | `pix shrink -s SRC -d OUT --force` |
|
|
165
|
+
| 删除源已消失的输出 | `pix shrink -s SRC -d OUT --prune` |
|
|
166
|
+
| 预览同步 | `pix backup -s SRC -d REMOTE --dry-run` |
|
|
167
|
+
| 首次运行或修改排除项后 | `pix backup -s SRC -d REMOTE --resync` |
|
|
168
|
+
| 输出 URL 清单 | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
169
|
+
| 诊断环境 | `pix doctor` |
|
|
170
|
+
| 生成配置文件 | `pix config init` |
|
|
171
|
+
|
|
172
|
+
## 安全边界
|
|
173
|
+
|
|
174
|
+
| 情况 | 会发生什么 | 如何防护 |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| 任意一侧删除文件 | 删除双向传播,误删不可逆 | 先 `--dry-run`;默认 `--max-delete 10`(百分比) |
|
|
177
|
+
| 首次同步 | rclone 没有基线,直接报错 | 显式加 `--resync` |
|
|
178
|
+
| 修改排除项 | 旧基线可能把被排除的文件当成删除传播 | 修改后必须重新 `--resync` |
|
|
179
|
+
| 源目录为空 | `backup` / `publish` 默认拒绝;`shrink --prune` 同样拒绝 | 仅在确认要清空时用 `--allow-empty`(同步还需 `--resync`) |
|
|
180
|
+
| `a.jpg` 与 `a.jpeg` 同时存在 | 映射到同一输出 | 编码前检测并以 2 退出 |
|
|
181
|
+
| 同时运行两个 shrink | 会写同一份状态和输出 | `OUT/.pix/lock` 阻止第二个进程 |
|
|
182
|
+
| 状态文件丢失或损坏 | 告警后全量重建 | 只会重新编码,不会产生错误结果 |
|
|
183
|
+
|
|
184
|
+
## 输出与退出码
|
|
185
|
+
|
|
186
|
+
- 日志和进度输出到 stderr。
|
|
187
|
+
- stdout 用于 `doctor` 报告、`--dry-run` 计划和 `--urls` JSON。
|
|
188
|
+
- 退出码:`0` 成功,`1` 运行失败,`2` 参数或配置错误。
|
|
189
|
+
- `backup` / `publish` 原样返回 rclone 的退出码。
|
|
190
|
+
|
|
191
|
+
URL 清单示例:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"2024/a.avif": "https://img.example.com/2024/a.avif"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 故障排查
|
|
200
|
+
|
|
201
|
+
| 现象 | 处理 |
|
|
202
|
+
|---|---|
|
|
203
|
+
| 提示 `pyvips/libvips unavailable` | 重装以带入 libvips:`uv tool install --force --editable /path/to/pix`;或手动安装 libvips(Linux:`apt install libvips42`;Windows:下载 libvips release 压缩包并把 `bin` 加入 PATH) |
|
|
204
|
+
| rclone 版本低于 1.66 | 升级 rclone |
|
|
205
|
+
| bisync 报缺少基线 | 首次运行加 `--resync` |
|
|
206
|
+
| `OUT/.pix/lock` 已存在 | 确认没有其他 `shrink` 在运行,再删除陈旧锁 |
|
|
207
|
+
| 配置解析失败 | 运行 `pix doctor` 定位文件;修复 TOML,或删除配置改用默认值 |
|
|
208
|
+
| `publish` 提示源目录没有 `.avif` | 先运行 `pix shrink` |
|
|
209
|
+
| URL 相关报错 | `base_url` 必须以 `http://` 或 `https://` 开头,且 `--urls` 依赖它 |
|
|
210
|
+
|
|
211
|
+
## 开发
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
uv sync
|
|
215
|
+
uv run pytest
|
|
216
|
+
uv run pix --version
|
|
217
|
+
uv build
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
代码必须兼容 Python 3.11。测试、打包与发布细节见[开发指南](https://github.com/uzvg/pix/blob/main/docs/development.md)。行为规格见[architecture.md](https://github.com/uzvg/pix/blob/main/docs/architecture.md),选型原因见[decisions.md](https://github.com/uzvg/pix/blob/main/docs/decisions.md)。
|
|
@@ -51,8 +51,8 @@ src/pix/
|
|
|
51
51
|
├── __init__.py # 包版本
|
|
52
52
|
├── py.typed
|
|
53
53
|
├── cli.py # 组合根:parser、main、子命令分派
|
|
54
|
-
├── config.py #
|
|
55
|
-
├── default_config.toml # config init
|
|
54
|
+
├── config.py # TOML 读取与校验、parse_size、config init
|
|
55
|
+
├── default_config.toml # 默认值唯一来源,也是 config init 的模板(随包分发)
|
|
56
56
|
├── logger.py # stderr 日志
|
|
57
57
|
├── shrink.py # shrink 子命令
|
|
58
58
|
├── rclone.py # rclone 探测、版本检查与执行
|
|
@@ -150,7 +150,7 @@ pix config init [--force]
|
|
|
150
150
|
- Linux/macOS:`$XDG_CONFIG_HOME/pix/config.toml`,未设置时为 `~/.config/pix/config.toml`。
|
|
151
151
|
- Windows:`%APPDATA%\pix\config.toml`。
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
文件不存在时使用内置默认值(即 `src/pix/default_config.toml`)。
|
|
154
154
|
|
|
155
155
|
```toml
|
|
156
156
|
[shrink]
|
|
@@ -230,6 +230,7 @@ binary = "rclone"
|
|
|
230
230
|
13. 汇总输出:转换、跳过(状态命中)、跳过(太小)、跳过(空源保护)、失败、删除。
|
|
231
231
|
14. 状态在编码过程中每成功 200 个文件刷新一次(原子写),并在正常结束或 `KeyboardInterrupt` 时通过 `finally` 再写一次,避免 Ctrl-C 丢掉整批进度。写入失败记录错误并返回 1,下次运行会重新编码未记录的文件。
|
|
232
232
|
15. 状态损坏、版本不识别或 JSON 解析失败时输出警告,按空状态全量重建,不中止。
|
|
233
|
+
16. 编码每个文件前在 stderr 输出进度 `processing <序号>/<总数>: <相对路径>`;终端下原地刷新,重定向时逐行输出,`--quiet` 抑制。日志与汇总会先结束进度行。
|
|
233
234
|
|
|
234
235
|
### 状态文件
|
|
235
236
|
|
|
@@ -343,7 +344,7 @@ config /home/uzvg/.config/pix/config.toml (ok)
|
|
|
343
344
|
|
|
344
345
|
## 9. 日志与退出码
|
|
345
346
|
|
|
346
|
-
-
|
|
347
|
+
- 默认:阶段信息、编码进度和汇总,输出到 stderr。终端下进度原地刷新,重定向时逐行输出。
|
|
347
348
|
- `-v`:输出每个文件的处理/跳过原因,以及 rclone 的进度。
|
|
348
349
|
- `--quiet`:只输出错误。
|
|
349
350
|
- `doctor` 报告输出到 stdout;`--dry-run` 的计划文件列表和 `--urls` 的 JSON 输出到 stdout。
|
|
@@ -172,6 +172,6 @@
|
|
|
172
172
|
|
|
173
173
|
## D28. 默认配置用内置模板生成
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
**决策**:随包分发的 `src/pix/default_config.toml` 是默认值的唯一来源:`load_settings` 通过 `load_defaults()` 解析它,`pix config init` 直接复制它(保留注释)。目标文件已存在时拒绝覆盖并退出 2,`--force` 才覆盖;该命令在 `load_settings` 之前分派。
|
|
176
176
|
|
|
177
|
-
**原因**:Python 3.11 标准库只有 `tomllib`,没有 TOML 写入器;引入 `tomli-w` 违反“仅标准库 + pyvips”的约束。独立文件可以直接带注释,用户打开就能改,编辑器也能按 TOML
|
|
177
|
+
**原因**:Python 3.11 标准库只有 `tomllib`,没有 TOML 写入器;引入 `tomli-w` 违反“仅标准库 + pyvips”的约束。独立文件可以直接带注释,用户打开就能改,编辑器也能按 TOML 高亮;默认值只在这一处维护,避免代码常量与模板漂移。拒绝覆盖保护用户已经修改过的配置,误覆盖的代价远高于多敲一个 `--force`。提前分派让 `--config` 指向新路径时也能创建工作,而不是被“config file not found”挡住。
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pix-cli"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Batch AVIF shrink and image-host sync CLI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "uzvg", email = "uzvg@qq.com" }
|
|
9
|
+
]
|
|
10
|
+
keywords = [
|
|
11
|
+
"avif",
|
|
12
|
+
"image-compression",
|
|
13
|
+
"image-host",
|
|
14
|
+
"rclone",
|
|
15
|
+
"cli",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: End Users/Desktop",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
29
|
+
"Topic :: System :: Archiving :: Backup",
|
|
30
|
+
]
|
|
31
|
+
requires-python = ">=3.11"
|
|
32
|
+
dependencies = [
|
|
33
|
+
"pyvips[binary]>=3.2",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
pix = "pix.cli:main"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/uzvg/pix"
|
|
41
|
+
Repository = "https://github.com/uzvg/pix"
|
|
42
|
+
Issues = "https://github.com/uzvg/pix/issues"
|
|
43
|
+
Changelog = "https://github.com/uzvg/pix/releases"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=8",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/pix"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
import copy
|
|
7
6
|
import os
|
|
8
7
|
import re
|
|
9
8
|
import sys
|
|
@@ -11,29 +10,6 @@ import tomllib
|
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from typing import Any
|
|
13
12
|
|
|
14
|
-
DEFAULTS: dict[str, Any] = {
|
|
15
|
-
"shrink": {
|
|
16
|
-
"width": 1920,
|
|
17
|
-
"quality": 85,
|
|
18
|
-
"effort": 5,
|
|
19
|
-
"min_size": 0,
|
|
20
|
-
"include": ["jpg", "jpeg", "png", "webp"],
|
|
21
|
-
},
|
|
22
|
-
"backup": {
|
|
23
|
-
"exclude": [],
|
|
24
|
-
"max_delete": 10,
|
|
25
|
-
},
|
|
26
|
-
"publish": {
|
|
27
|
-
"base_url": "",
|
|
28
|
-
"urls": False,
|
|
29
|
-
"exclude": [],
|
|
30
|
-
"max_delete": 10,
|
|
31
|
-
},
|
|
32
|
-
"rclone": {
|
|
33
|
-
"binary": "rclone",
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
|
|
37
13
|
_SIZE_RE = re.compile(r"^(\d+)([KMG]?)$", re.IGNORECASE)
|
|
38
14
|
_SIZE_UNITS = {"": 1, "K": 1024, "M": 1024**2, "G": 1024**3}
|
|
39
15
|
|
|
@@ -42,6 +18,18 @@ class ConfigError(Exception):
|
|
|
42
18
|
"""Invalid or unreadable configuration."""
|
|
43
19
|
|
|
44
20
|
|
|
21
|
+
def load_defaults() -> dict[str, Any]:
|
|
22
|
+
"""Read the packaged template, the single source of default values."""
|
|
23
|
+
path = Path(__file__).with_name("default_config.toml")
|
|
24
|
+
try:
|
|
25
|
+
with path.open("rb") as handle:
|
|
26
|
+
return tomllib.load(handle)
|
|
27
|
+
except OSError as exc:
|
|
28
|
+
raise ConfigError(f"{path}: {exc}") from exc
|
|
29
|
+
except tomllib.TOMLDecodeError as exc:
|
|
30
|
+
raise ConfigError(f"{path}: {exc}") from exc
|
|
31
|
+
|
|
32
|
+
|
|
45
33
|
def parse_size(value: str | int) -> int:
|
|
46
34
|
if isinstance(value, int) and not isinstance(value, bool):
|
|
47
35
|
if value < 0:
|
|
@@ -97,7 +85,7 @@ def load_settings(explicit: Path | None = None) -> dict[str, Any]:
|
|
|
97
85
|
if explicit is not None and not path.is_file():
|
|
98
86
|
raise ConfigError(f"config file not found: {path}")
|
|
99
87
|
overrides = load_config(path)
|
|
100
|
-
settings =
|
|
88
|
+
settings = load_defaults()
|
|
101
89
|
_deep_update(settings, overrides)
|
|
102
90
|
validate_settings(settings)
|
|
103
91
|
return settings
|
|
@@ -9,18 +9,37 @@ class Logger:
|
|
|
9
9
|
def __init__(self, verbose: bool = False, quiet: bool = False) -> None:
|
|
10
10
|
self.verbose = verbose
|
|
11
11
|
self.quiet = quiet
|
|
12
|
+
self._progress_active = False
|
|
13
|
+
|
|
14
|
+
def _end_progress(self) -> None:
|
|
15
|
+
if self._progress_active:
|
|
16
|
+
print(file=sys.stderr)
|
|
17
|
+
self._progress_active = False
|
|
12
18
|
|
|
13
19
|
def info(self, message: str) -> None:
|
|
14
20
|
if not self.quiet:
|
|
21
|
+
self._end_progress()
|
|
15
22
|
print(message, file=sys.stderr)
|
|
16
23
|
|
|
17
24
|
def detail(self, message: str) -> None:
|
|
18
25
|
if self.verbose and not self.quiet:
|
|
26
|
+
self._end_progress()
|
|
19
27
|
print(message, file=sys.stderr)
|
|
20
28
|
|
|
21
29
|
def warning(self, message: str) -> None:
|
|
22
30
|
if not self.quiet:
|
|
31
|
+
self._end_progress()
|
|
23
32
|
print(f"pix: warning: {message}", file=sys.stderr)
|
|
24
33
|
|
|
25
34
|
def error(self, message: str) -> None:
|
|
35
|
+
self._end_progress()
|
|
26
36
|
print(f"pix: error: {message}", file=sys.stderr)
|
|
37
|
+
|
|
38
|
+
def progress(self, message: str) -> None:
|
|
39
|
+
if self.quiet:
|
|
40
|
+
return
|
|
41
|
+
if sys.stderr.isatty():
|
|
42
|
+
print(f"\r{message}\033[K", end="", file=sys.stderr, flush=True)
|
|
43
|
+
self._progress_active = True
|
|
44
|
+
else:
|
|
45
|
+
print(message, file=sys.stderr)
|
|
@@ -258,9 +258,10 @@ def shrink(args: argparse.Namespace, settings: dict[str, Any], log: Logger) -> i
|
|
|
258
258
|
status = 0
|
|
259
259
|
pyvips = None
|
|
260
260
|
encoded: set[str] = set()
|
|
261
|
+
total = len(sources)
|
|
261
262
|
|
|
262
263
|
try:
|
|
263
|
-
for src in sources:
|
|
264
|
+
for index, src in enumerate(sources, 1):
|
|
264
265
|
rel = src.relative_to(source).as_posix()
|
|
265
266
|
try:
|
|
266
267
|
src_stat = src.stat()
|
|
@@ -287,6 +288,8 @@ def shrink(args: argparse.Namespace, settings: dict[str, Any], log: Logger) -> i
|
|
|
287
288
|
counters["converted"] += 1
|
|
288
289
|
continue
|
|
289
290
|
|
|
291
|
+
log.progress(f"processing {index}/{total}: {rel}")
|
|
292
|
+
|
|
290
293
|
if pyvips is None:
|
|
291
294
|
try:
|
|
292
295
|
import pyvips as pyvips_module
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import tomllib
|
|
@@ -7,6 +8,7 @@ import pytest
|
|
|
7
8
|
|
|
8
9
|
from pix import cli, doctor, rclone
|
|
9
10
|
from pix import config as pix_config
|
|
11
|
+
from pix.logger import Logger
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def test_parse_size():
|
|
@@ -20,6 +22,22 @@ def test_parse_size():
|
|
|
20
22
|
pix_config.parse_size(bad)
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
def test_logger_progress_rewrites_on_tty(monkeypatch):
|
|
26
|
+
class FakeStderr(io.StringIO):
|
|
27
|
+
def isatty(self):
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
stream = FakeStderr()
|
|
31
|
+
monkeypatch.setattr("pix.logger.sys.stderr", stream)
|
|
32
|
+
|
|
33
|
+
log = Logger()
|
|
34
|
+
log.progress("first")
|
|
35
|
+
log.progress("second")
|
|
36
|
+
log.info("done")
|
|
37
|
+
|
|
38
|
+
assert stream.getvalue() == "\rfirst\033[K\rsecond\033[K\ndone\n"
|
|
39
|
+
|
|
40
|
+
|
|
23
41
|
def test_version_exits_zero(capsys):
|
|
24
42
|
assert cli.main(["--version"]) == 0
|
|
25
43
|
assert capsys.readouterr().out.strip() == f"pix {cli.__version__}"
|
|
@@ -38,7 +56,7 @@ def test_config_priority_and_defaults(tmp_path: Path):
|
|
|
38
56
|
|
|
39
57
|
assert settings["shrink"]["width"] == 1000
|
|
40
58
|
assert settings["shrink"]["quality"] == 70
|
|
41
|
-
assert settings["shrink"]["effort"] == pix_config.
|
|
59
|
+
assert settings["shrink"]["effort"] == pix_config.load_defaults()["shrink"]["effort"]
|
|
42
60
|
assert settings["publish"]["max_delete"] == 10
|
|
43
61
|
|
|
44
62
|
|
|
@@ -74,7 +92,7 @@ def test_config_init_writes_defaults(tmp_path: Path, capsys):
|
|
|
74
92
|
assert cli.main(["--config", str(config), "config", "init"]) == 0
|
|
75
93
|
|
|
76
94
|
assert capsys.readouterr().out.strip() == f"wrote {config}"
|
|
77
|
-
assert tomllib.loads(config.read_text(encoding="utf-8")) == pix_config.
|
|
95
|
+
assert tomllib.loads(config.read_text(encoding="utf-8")) == pix_config.load_defaults()
|
|
78
96
|
assert pix_config.load_settings(config)["shrink"]["width"] == 1920
|
|
79
97
|
|
|
80
98
|
|
|
@@ -94,7 +112,7 @@ def test_config_init_force_overwrites(tmp_path: Path, capsys):
|
|
|
94
112
|
|
|
95
113
|
assert cli.main(["--config", str(config), "config", "init", "--force"]) == 0
|
|
96
114
|
|
|
97
|
-
assert tomllib.loads(config.read_text(encoding="utf-8")) == pix_config.
|
|
115
|
+
assert tomllib.loads(config.read_text(encoding="utf-8")) == pix_config.load_defaults()
|
|
98
116
|
assert capsys.readouterr().out.strip() == f"wrote {config}"
|
|
99
117
|
|
|
100
118
|
|
|
@@ -142,6 +160,16 @@ def test_shrink_encodes_and_writes_state(tmp_path: Path):
|
|
|
142
160
|
assert entry["out_size"] == output.stat().st_size
|
|
143
161
|
|
|
144
162
|
|
|
163
|
+
def test_shrink_reports_progress(tmp_path: Path, capsys):
|
|
164
|
+
source = tmp_path / "src"
|
|
165
|
+
dest = tmp_path / "out"
|
|
166
|
+
make_png(source / "a.png")
|
|
167
|
+
|
|
168
|
+
assert run_shrink(source, dest, "-w", "100") == 0
|
|
169
|
+
|
|
170
|
+
assert "processing 1/1: a.png" in capsys.readouterr().err
|
|
171
|
+
|
|
172
|
+
|
|
145
173
|
def test_shrink_second_run_skips_unchanged(tmp_path: Path, capsys):
|
|
146
174
|
source = tmp_path / "src"
|
|
147
175
|
dest = tmp_path / "out"
|
pix_cli-0.1.0/PKG-INFO
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: pix-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Batch AVIF shrink and image-host sync CLI
|
|
5
|
-
Author-email: uzvg <uzvg@qq.com>
|
|
6
|
-
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: pyvips[binary]>=3.2
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
# pix
|
|
11
|
-
|
|
12
|
-
Batch AVIF shrink and image-host sync CLI.
|
|
13
|
-
|
|
14
|
-
`pix` 把本地图片目录增量压缩为 AVIF,再通过 rclone bisync 备份原图、发布压缩图到图床。只处理显式传入的路径和远端。
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
pix shrink -s ./photos -d ./out -w 1920 -q 85
|
|
18
|
-
pix backup -s ./photos -d onedrive:photos --resync
|
|
19
|
-
pix publish -s ./out -d r2:images --base-url https://img.example.com --urls
|
|
20
|
-
pix doctor
|
|
21
|
-
pix config init
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
`backup` / `publish` 是双向镜像,不是单向备份:删除会双向传播。默认 `--max-delete 10`(删除百分比,即 10%),超过该比例时 rclone 会拒绝;首次运行或修改排除项后必须显式 `--resync`,先跑 `--dry-run` 更安全。
|
|
25
|
-
|
|
26
|
-
> Status: implementation complete; real R2 and Windows smoke tests pending (see roadmap).
|
|
27
|
-
|
|
28
|
-
## Docs
|
|
29
|
-
|
|
30
|
-
- [Architecture](docs/architecture.md) — behavior spec
|
|
31
|
-
- [Development](docs/development.md) — setup, tests, build
|
|
32
|
-
- [Decisions](docs/decisions.md) — technical decisions and rationale
|
|
33
|
-
- [Roadmap](docs/roadmap.md) — status and tasks
|
|
34
|
-
|
|
35
|
-
## Quick start
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
uv sync
|
|
39
|
-
uv run pytest
|
|
40
|
-
```
|
pix_cli-0.1.0/README.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# pix
|
|
2
|
-
|
|
3
|
-
Batch AVIF shrink and image-host sync CLI.
|
|
4
|
-
|
|
5
|
-
`pix` 把本地图片目录增量压缩为 AVIF,再通过 rclone bisync 备份原图、发布压缩图到图床。只处理显式传入的路径和远端。
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pix shrink -s ./photos -d ./out -w 1920 -q 85
|
|
9
|
-
pix backup -s ./photos -d onedrive:photos --resync
|
|
10
|
-
pix publish -s ./out -d r2:images --base-url https://img.example.com --urls
|
|
11
|
-
pix doctor
|
|
12
|
-
pix config init
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
`backup` / `publish` 是双向镜像,不是单向备份:删除会双向传播。默认 `--max-delete 10`(删除百分比,即 10%),超过该比例时 rclone 会拒绝;首次运行或修改排除项后必须显式 `--resync`,先跑 `--dry-run` 更安全。
|
|
16
|
-
|
|
17
|
-
> Status: implementation complete; real R2 and Windows smoke tests pending (see roadmap).
|
|
18
|
-
|
|
19
|
-
## Docs
|
|
20
|
-
|
|
21
|
-
- [Architecture](docs/architecture.md) — behavior spec
|
|
22
|
-
- [Development](docs/development.md) — setup, tests, build
|
|
23
|
-
- [Decisions](docs/decisions.md) — technical decisions and rationale
|
|
24
|
-
- [Roadmap](docs/roadmap.md) — status and tasks
|
|
25
|
-
|
|
26
|
-
## Quick start
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
uv sync
|
|
30
|
-
uv run pytest
|
|
31
|
-
```
|
pix_cli-0.1.0/pyproject.toml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "pix-cli"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
description = "Batch AVIF shrink and image-host sync CLI"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
authors = [
|
|
7
|
-
{ name = "uzvg", email = "uzvg@qq.com" }
|
|
8
|
-
]
|
|
9
|
-
requires-python = ">=3.11"
|
|
10
|
-
dependencies = [
|
|
11
|
-
"pyvips[binary]>=3.2",
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
[project.scripts]
|
|
15
|
-
pix = "pix.cli:main"
|
|
16
|
-
|
|
17
|
-
[dependency-groups]
|
|
18
|
-
dev = [
|
|
19
|
-
"pytest>=8",
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
[build-system]
|
|
23
|
-
requires = ["hatchling"]
|
|
24
|
-
build-backend = "hatchling.build"
|
|
25
|
-
|
|
26
|
-
[tool.hatch.build.targets.wheel]
|
|
27
|
-
packages = ["src/pix"]
|
|
28
|
-
|
|
29
|
-
[tool.pytest.ini_options]
|
|
30
|
-
testpaths = ["tests"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|