pix-cli 0.1.2__tar.gz → 0.1.4__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 → pix_cli-0.1.4}/AGENTS.md +6 -5
- {pix_cli-0.1.2 → pix_cli-0.1.4}/PKG-INFO +54 -38
- {pix_cli-0.1.2 → pix_cli-0.1.4}/README.md +52 -36
- {pix_cli-0.1.2 → pix_cli-0.1.4}/README.zh-CN.md +52 -36
- {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/architecture.md +77 -31
- {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/decisions.md +37 -13
- {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/development.md +12 -9
- {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/roadmap.md +25 -15
- {pix_cli-0.1.2 → pix_cli-0.1.4}/pyproject.toml +2 -2
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/__init__.py +1 -1
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/cli.py +11 -11
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/config.py +21 -2
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/default_config.toml +7 -1
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/logger.py +3 -0
- pix_cli-0.1.4/src/pix/rclone.py +162 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/sync.py +112 -32
- {pix_cli-0.1.2 → pix_cli-0.1.4}/tests/test_cli.py +269 -38
- {pix_cli-0.1.2 → pix_cli-0.1.4}/uv.lock +1 -1
- pix_cli-0.1.2/src/pix/rclone.py +0 -72
- {pix_cli-0.1.2 → pix_cli-0.1.4}/.github/workflows/ci.yml +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/.github/workflows/publish.yml +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/.gitignore +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/.python-version +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/LICENSE +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/doctor.py +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/py.typed +0 -0
- {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/shrink.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
`pix` 是批量图片压缩(AVIF)与 rclone 图床同步 CLI。已实现 `shrink` / `
|
|
3
|
+
`pix` 是批量图片压缩(AVIF)与 rclone 图床同步 CLI。已实现 `shrink` / `mirror` / `publish` / `doctor` / `config init`;行为规格见 `docs/architecture.md`。
|
|
4
4
|
|
|
5
5
|
## 文档
|
|
6
6
|
|
|
@@ -31,10 +31,11 @@ uv build
|
|
|
31
31
|
- `pyvips` 只在 `shrink` / `doctor` 内延迟导入。
|
|
32
32
|
- 不通过 shell 调用 `vips` 或 `rclone`。
|
|
33
33
|
- `-q` 是 quality,`--quiet` 是静默。
|
|
34
|
-
- 退出码 0/1/2,日志走 stderr;`
|
|
35
|
-
- `
|
|
36
|
-
- `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`
|
|
37
|
-
-
|
|
34
|
+
- 退出码 0/1/2,日志走 stderr;`mirror` / `publish` 透传 rclone 退出码。
|
|
35
|
+
- `mirror` / `publish` 使用 `rclone bisync`;调用前先 `rclone mkdir` 目标并用 `rclone lsjson --max-depth 1` 判空。目标为空时自动加 `--resync`,目标非空时必须显式 `--resync`。用户不需要手动运行 rclone。它们是双向镜像,不是单向备份。
|
|
36
|
+
- `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`mirror` / `publish` 源为空时默认拒绝,`--allow-empty` 加 `--resync` 才放行。
|
|
37
|
+
- `mirror` 默认只同步 `[mirror].include` 中的图片扩展名,`include = []` 关闭白名单;`publish` 固定只同步 `.avif`。两者用 rclone filters 文件(bisync `--filters-file`、判空 `--filter-from`)并加 `--ignore-case`。
|
|
38
|
+
- 修改 include/exclude 后必须提示用户先 `--resync`;升级到带过滤的版本后首次运行也需要 `--resync`。`--exclude` 是配置在前、命令行追加并去重。
|
|
38
39
|
- 增量状态固定在 `OUT/.pix/state.json`,每 200 个成功文件刷新并在 `finally` 落盘;`publish` 必须排除 `.pix/**`。
|
|
39
40
|
- `shrink` 必须拒绝目标冲突,`--prune` 必须拒绝空源,非 dry-run 必须持有 `OUT/.pix/lock`。
|
|
40
41
|
- `publish` 源必须至少有一个 `.avif`;`base_url` 必须以 `http://` 或 `https://` 开头。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pix-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Batch AVIF shrink and image-host sync CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/uzvg/pix
|
|
6
6
|
Project-URL: Repository, https://github.com/uzvg/pix
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
24
|
-
Classifier: Topic :: System :: Archiving ::
|
|
24
|
+
Classifier: Topic :: System :: Archiving :: Mirroring
|
|
25
25
|
Requires-Python: >=3.11
|
|
26
26
|
Requires-Dist: pyvips[binary]>=3.2
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
@@ -41,9 +41,9 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
| Command | Purpose |
|
|
42
42
|
|---|---|
|
|
43
43
|
| `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
|
|
44
|
-
| `pix
|
|
44
|
+
| `pix mirror` | Bidirectionally mirror an original photo directory to a cloud drive |
|
|
45
45
|
| `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
|
|
46
|
-
| `pix doctor` |
|
|
46
|
+
| `pix doctor` | Report Python, libvips and rclone versions, plus the config path |
|
|
47
47
|
| `pix config init` | Write a commented default configuration file |
|
|
48
48
|
|
|
49
49
|
`pix` is deliberately narrow:
|
|
@@ -58,32 +58,41 @@ Description-Content-Type: text/markdown
|
|
|
58
58
|
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
59
59
|
│
|
|
60
60
|
└── publish (rclone bisync) ──> r2:images
|
|
61
|
-
photos/ ──
|
|
61
|
+
photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
| Command | What actually runs | Who does the work |
|
|
65
65
|
|---|---|---|
|
|
66
66
|
| `shrink` | pyvips thumbnail + AVIF encode | pix |
|
|
67
|
-
| `
|
|
67
|
+
| `mirror` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
|
|
68
68
|
| `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
|
|
69
|
-
| `doctor` / `config init` |
|
|
69
|
+
| `doctor` / `config init` | Runtime report / config scaffold | pix |
|
|
70
70
|
|
|
71
|
-
**Sync is rclone.** `
|
|
71
|
+
**Sync is rclone.** `mirror` 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
72
|
|
|
73
|
-
`
|
|
73
|
+
`mirror` / `publish` are **bidirectional mirrors**, not one-way copies. Deletions propagate in both directions.
|
|
74
74
|
|
|
75
75
|
## Requirements
|
|
76
76
|
|
|
77
77
|
- Python 3.11+
|
|
78
78
|
- [uv](https://docs.astral.sh/uv/)
|
|
79
|
-
- rclone >= 1.66 for `
|
|
79
|
+
- rclone >= 1.66 for `mirror` / `publish` (and for `pix doctor` to report a clean environment)
|
|
80
80
|
- At least one remote already configured in rclone
|
|
81
81
|
|
|
82
82
|
`pyvips[binary]` bundles libvips, so no separate vips installation is required.
|
|
83
83
|
|
|
84
84
|
## Install
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Install from PyPI:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv tool install pix-cli
|
|
90
|
+
pix --version
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Upgrade with `uv tool upgrade pix-cli`; remove with `uv tool uninstall pix-cli`. Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`.
|
|
94
|
+
|
|
95
|
+
To work on pix itself, install the checkout in editable mode:
|
|
87
96
|
|
|
88
97
|
```bash
|
|
89
98
|
git clone https://github.com/uzvg/pix.git
|
|
@@ -92,11 +101,11 @@ uv tool install --editable .
|
|
|
92
101
|
pix --version
|
|
93
102
|
```
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`.
|
|
96
105
|
|
|
97
106
|
## Configure rclone
|
|
98
107
|
|
|
99
|
-
Only needed for `
|
|
108
|
+
Only needed for `mirror` / `publish`:
|
|
100
109
|
|
|
101
110
|
```bash
|
|
102
111
|
rclone config # create a remote, for example onedrive or r2
|
|
@@ -118,7 +127,7 @@ If the file is missing, built-in defaults are used. Generate a commented file wi
|
|
|
118
127
|
pix config init
|
|
119
128
|
```
|
|
120
129
|
|
|
121
|
-
Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file.
|
|
130
|
+
Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file; an explicit path must exist, while a missing default path is fine.
|
|
122
131
|
|
|
123
132
|
Common keys:
|
|
124
133
|
|
|
@@ -129,15 +138,18 @@ Common keys:
|
|
|
129
138
|
| | `effort` | `5` | libvips effort, 0–9 |
|
|
130
139
|
| | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
|
|
131
140
|
| | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
|
|
132
|
-
| `[
|
|
141
|
+
| `[mirror]` | `include` | common image extensions | Extensions to mirror, without dots; `[]` disables the whitelist |
|
|
142
|
+
| | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
|
|
133
143
|
| | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
|
|
134
144
|
| `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
|
|
135
145
|
| | `urls` | `false` | Emit a JSON URL manifest |
|
|
136
|
-
| | `exclude` | `[]` | Same as
|
|
137
|
-
| | `max_delete` | `10` | Same as
|
|
146
|
+
| | `exclude` | `[]` | Same as mirror; `.pix/**` is always excluded first |
|
|
147
|
+
| | `max_delete` | `10` | Same as mirror |
|
|
138
148
|
| `[rclone]` | `binary` | `rclone` | rclone executable name or path |
|
|
139
149
|
|
|
140
|
-
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality;
|
|
150
|
+
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; `--quiet` is errors-only; `-v` / `--verbose` adds per-file detail and rclone progress. `--no-urls` overrides `[publish].urls = true` for one run.
|
|
151
|
+
|
|
152
|
+
The default `[mirror].include` is `jpg`, `jpeg`, `png`, `webp`, `gif`, `bmp`, `tif`, `tiff`, `heic`, `heif`, `avif`, `jxl`, `svg`, `ico`, `cr2`, `cr3`, `nef`, `arw`, `dng`, `orf`, `raf`, `rw2`. Set it to `[]` to mirror every file.
|
|
141
153
|
|
|
142
154
|
## Quick start
|
|
143
155
|
|
|
@@ -154,16 +166,16 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
154
166
|
pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
|
|
155
167
|
```
|
|
156
168
|
|
|
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.
|
|
169
|
+
The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. `-w` is a maximum long edge, so smaller images are not upscaled; relative directories are preserved under OUT. Change detection uses source size/mtime and output size, not content hashes. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan. `--prune` removes only outputs recorded in state; extra files in OUT are left alone.
|
|
158
170
|
|
|
159
|
-
3.
|
|
171
|
+
3. Mirror originals:
|
|
160
172
|
|
|
161
173
|
```bash
|
|
162
|
-
pix
|
|
163
|
-
pix
|
|
174
|
+
pix mirror -s ~/Pictures -d onedrive:photos --dry-run
|
|
175
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
164
176
|
```
|
|
165
177
|
|
|
166
|
-
`--resync` is
|
|
178
|
+
The destination is created automatically. Only files matching `[mirror].include` are mirrored; other source files are ignored, and non-matching files already on the destination are left untouched. If no source file matches the filter, the source counts as empty and is refused unless you pass `--allow-empty --resync`. An empty destination initializes its own baseline, so no `--resync` is needed on the first run. Use `--resync` to rebuild the baseline when the destination already has matching content, or after changing `include`/`exclude`; `--dry-run` may create an empty destination but does not transfer or delete. After upgrading from a version without include filtering, the first run also needs `--resync`; preview with `--dry-run --resync` first (the preview does not write the baseline).
|
|
167
179
|
|
|
168
180
|
4. Publish the AVIF output:
|
|
169
181
|
|
|
@@ -172,17 +184,18 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
172
184
|
--base-url https://img.example.com --urls > urls.json
|
|
173
185
|
```
|
|
174
186
|
|
|
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.
|
|
187
|
+
`publish` requires at least one `.avif` in the source unless you pass `--allow-empty --resync`; it syncs only `.avif` files and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync. It lists every `.avif` under OUT, not just files changed in this run, and is not filtered by `[publish].exclude` or `--exclude`; excluded files can still appear in it. If you rely on excludes to hide files, do not publish the manifest until this behavior is fixed.
|
|
176
188
|
|
|
177
189
|
5. Re-run later:
|
|
178
190
|
|
|
179
191
|
```bash
|
|
180
192
|
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
181
|
-
pix
|
|
182
|
-
pix publish -s ~/Pictures-avif -d r2:images
|
|
193
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
194
|
+
pix publish -s ~/Pictures-avif -d r2:images \
|
|
195
|
+
--base-url https://img.example.com --urls
|
|
183
196
|
```
|
|
184
197
|
|
|
185
|
-
Only changes are encoded and transferred.
|
|
198
|
+
Only changes are encoded and transferred. `--base-url` is not persisted between runs; set `[publish].base_url` in config to avoid repeating it.
|
|
186
199
|
|
|
187
200
|
## Command cheat sheet
|
|
188
201
|
|
|
@@ -190,9 +203,10 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
190
203
|
|---|---|
|
|
191
204
|
| Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
|
|
192
205
|
| 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
|
|
195
|
-
| First
|
|
206
|
+
| Remove tracked outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
|
|
207
|
+
| Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
|
|
208
|
+
| First sync (new or empty destination) | `pix mirror -s SRC -d REMOTE` |
|
|
209
|
+
| Rebuild the baseline (non-empty destination, or after changing include/exclude) | `pix mirror -s SRC -d REMOTE --resync` |
|
|
196
210
|
| Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
197
211
|
| Diagnose the environment | `pix doctor` |
|
|
198
212
|
| Generate a config file | `pix config init` |
|
|
@@ -202,19 +216,21 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
202
216
|
| Situation | What happens | Protection |
|
|
203
217
|
|---|---|---|
|
|
204
218
|
| Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
|
|
205
|
-
| First sync |
|
|
206
|
-
|
|
|
207
|
-
|
|
|
219
|
+
| First sync (destination empty) | `pix` creates the destination and initializes the baseline | Preview with `--dry-run`; it may create an empty destination but does not transfer or delete. No manual rclone needed |
|
|
220
|
+
| Destination has matching content but no baseline | `pix` does not auto-resync; bisync fails instead of overwriting | Confirm which side is authoritative, then add `--resync` |
|
|
221
|
+
| Include/exclude filters changed | When filters are active, rclone detects the changed filters file and exits 7 | Run with `--resync` after changing `include` or `exclude`; `--dry-run --resync` previews but does not write the baseline |
|
|
222
|
+
| Non-image files in the source | `mirror` ignores them by default; `publish` ignores anything that is not `.avif`. If no file matches, the source counts as empty and is refused | Add extensions to `[mirror].include` if they should be mirrored; use `--allow-empty --resync` only for an intentional empty sync |
|
|
223
|
+
| Empty source | `mirror` / `publish` refuse. `shrink --prune` refuses only while state still has entries; empty state is a no-op | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
|
|
208
224
|
| `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
|
|
209
225
|
| Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
|
|
210
|
-
| State
|
|
226
|
+
| State missing or corrupt | Full rebuild; corrupt state logs a warning | Only causes re-encoding; outputs stay valid |
|
|
211
227
|
|
|
212
228
|
## Output and exit codes
|
|
213
229
|
|
|
214
230
|
- Logs and progress go to stderr.
|
|
215
|
-
- stdout carries `doctor` reports,
|
|
231
|
+
- stdout carries `doctor` reports, `shrink --dry-run` plans, `config init`'s `wrote <path>`, and `--urls` JSON. `mirror` / `publish --dry-run` output comes from rclone.
|
|
216
232
|
- Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
|
|
217
|
-
- `
|
|
233
|
+
- `mirror` / `publish` return rclone's bisync exit code unchanged; destination preflight failures return 1.
|
|
218
234
|
|
|
219
235
|
URL manifest example:
|
|
220
236
|
|
|
@@ -230,9 +246,9 @@ URL manifest example:
|
|
|
230
246
|
|---|---|
|
|
231
247
|
| `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
248
|
| rclone is older than 1.66 | Upgrade rclone |
|
|
233
|
-
| bisync reports a missing baseline |
|
|
249
|
+
| bisync reports a missing baseline or changed filters file | The destination has matching content, or the include/exclude rules changed (including the first run after upgrading). Confirm local should win, then add `--resync`; an empty destination initializes automatically. `--dry-run --resync` previews but does not write the baseline. |
|
|
234
250
|
| `OUT/.pix/lock` already exists | Check for another `shrink`, then remove a stale lock |
|
|
235
|
-
| Configuration fails to parse |
|
|
251
|
+
| Configuration fails to parse | The error names the file and location; fix the TOML or remove the file to fall back to defaults. `pix doctor` cannot run until the config parses |
|
|
236
252
|
| `publish` says the source has no `.avif` | Run `pix shrink` first |
|
|
237
253
|
| URL-related error | `base_url` must start with `http://` or `https://`, and `--urls` requires it |
|
|
238
254
|
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
| Command | Purpose |
|
|
14
14
|
|---|---|
|
|
15
15
|
| `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
|
|
16
|
-
| `pix
|
|
16
|
+
| `pix mirror` | Bidirectionally mirror an original photo directory to a cloud drive |
|
|
17
17
|
| `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
|
|
18
|
-
| `pix doctor` |
|
|
18
|
+
| `pix doctor` | Report Python, libvips and rclone versions, plus the config path |
|
|
19
19
|
| `pix config init` | Write a commented default configuration file |
|
|
20
20
|
|
|
21
21
|
`pix` is deliberately narrow:
|
|
@@ -30,32 +30,41 @@
|
|
|
30
30
|
photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
|
|
31
31
|
│
|
|
32
32
|
└── publish (rclone bisync) ──> r2:images
|
|
33
|
-
photos/ ──
|
|
33
|
+
photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
| Command | What actually runs | Who does the work |
|
|
37
37
|
|---|---|---|
|
|
38
38
|
| `shrink` | pyvips thumbnail + AVIF encode | pix |
|
|
39
|
-
| `
|
|
39
|
+
| `mirror` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
|
|
40
40
|
| `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
|
|
41
|
-
| `doctor` / `config init` |
|
|
41
|
+
| `doctor` / `config init` | Runtime report / config scaffold | pix |
|
|
42
42
|
|
|
43
|
-
**Sync is rclone.** `
|
|
43
|
+
**Sync is rclone.** `mirror` 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
44
|
|
|
45
|
-
`
|
|
45
|
+
`mirror` / `publish` are **bidirectional mirrors**, not one-way copies. Deletions propagate in both directions.
|
|
46
46
|
|
|
47
47
|
## Requirements
|
|
48
48
|
|
|
49
49
|
- Python 3.11+
|
|
50
50
|
- [uv](https://docs.astral.sh/uv/)
|
|
51
|
-
- rclone >= 1.66 for `
|
|
51
|
+
- rclone >= 1.66 for `mirror` / `publish` (and for `pix doctor` to report a clean environment)
|
|
52
52
|
- At least one remote already configured in rclone
|
|
53
53
|
|
|
54
54
|
`pyvips[binary]` bundles libvips, so no separate vips installation is required.
|
|
55
55
|
|
|
56
56
|
## Install
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Install from PyPI:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv tool install pix-cli
|
|
62
|
+
pix --version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Upgrade with `uv tool upgrade pix-cli`; remove with `uv tool uninstall pix-cli`. Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`.
|
|
66
|
+
|
|
67
|
+
To work on pix itself, install the checkout in editable mode:
|
|
59
68
|
|
|
60
69
|
```bash
|
|
61
70
|
git clone https://github.com/uzvg/pix.git
|
|
@@ -64,11 +73,11 @@ uv tool install --editable .
|
|
|
64
73
|
pix --version
|
|
65
74
|
```
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`.
|
|
68
77
|
|
|
69
78
|
## Configure rclone
|
|
70
79
|
|
|
71
|
-
Only needed for `
|
|
80
|
+
Only needed for `mirror` / `publish`:
|
|
72
81
|
|
|
73
82
|
```bash
|
|
74
83
|
rclone config # create a remote, for example onedrive or r2
|
|
@@ -90,7 +99,7 @@ If the file is missing, built-in defaults are used. Generate a commented file wi
|
|
|
90
99
|
pix config init
|
|
91
100
|
```
|
|
92
101
|
|
|
93
|
-
Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file.
|
|
102
|
+
Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file; an explicit path must exist, while a missing default path is fine.
|
|
94
103
|
|
|
95
104
|
Common keys:
|
|
96
105
|
|
|
@@ -101,15 +110,18 @@ Common keys:
|
|
|
101
110
|
| | `effort` | `5` | libvips effort, 0–9 |
|
|
102
111
|
| | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
|
|
103
112
|
| | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
|
|
104
|
-
| `[
|
|
113
|
+
| `[mirror]` | `include` | common image extensions | Extensions to mirror, without dots; `[]` disables the whitelist |
|
|
114
|
+
| | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
|
|
105
115
|
| | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
|
|
106
116
|
| `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
|
|
107
117
|
| | `urls` | `false` | Emit a JSON URL manifest |
|
|
108
|
-
| | `exclude` | `[]` | Same as
|
|
109
|
-
| | `max_delete` | `10` | Same as
|
|
118
|
+
| | `exclude` | `[]` | Same as mirror; `.pix/**` is always excluded first |
|
|
119
|
+
| | `max_delete` | `10` | Same as mirror |
|
|
110
120
|
| `[rclone]` | `binary` | `rclone` | rclone executable name or path |
|
|
111
121
|
|
|
112
|
-
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality;
|
|
122
|
+
Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; `--quiet` is errors-only; `-v` / `--verbose` adds per-file detail and rclone progress. `--no-urls` overrides `[publish].urls = true` for one run.
|
|
123
|
+
|
|
124
|
+
The default `[mirror].include` is `jpg`, `jpeg`, `png`, `webp`, `gif`, `bmp`, `tif`, `tiff`, `heic`, `heif`, `avif`, `jxl`, `svg`, `ico`, `cr2`, `cr3`, `nef`, `arw`, `dng`, `orf`, `raf`, `rw2`. Set it to `[]` to mirror every file.
|
|
113
125
|
|
|
114
126
|
## Quick start
|
|
115
127
|
|
|
@@ -126,16 +138,16 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
126
138
|
pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
|
|
127
139
|
```
|
|
128
140
|
|
|
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.
|
|
141
|
+
The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. `-w` is a maximum long edge, so smaller images are not upscaled; relative directories are preserved under OUT. Change detection uses source size/mtime and output size, not content hashes. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan. `--prune` removes only outputs recorded in state; extra files in OUT are left alone.
|
|
130
142
|
|
|
131
|
-
3.
|
|
143
|
+
3. Mirror originals:
|
|
132
144
|
|
|
133
145
|
```bash
|
|
134
|
-
pix
|
|
135
|
-
pix
|
|
146
|
+
pix mirror -s ~/Pictures -d onedrive:photos --dry-run
|
|
147
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
136
148
|
```
|
|
137
149
|
|
|
138
|
-
`--resync` is
|
|
150
|
+
The destination is created automatically. Only files matching `[mirror].include` are mirrored; other source files are ignored, and non-matching files already on the destination are left untouched. If no source file matches the filter, the source counts as empty and is refused unless you pass `--allow-empty --resync`. An empty destination initializes its own baseline, so no `--resync` is needed on the first run. Use `--resync` to rebuild the baseline when the destination already has matching content, or after changing `include`/`exclude`; `--dry-run` may create an empty destination but does not transfer or delete. After upgrading from a version without include filtering, the first run also needs `--resync`; preview with `--dry-run --resync` first (the preview does not write the baseline).
|
|
139
151
|
|
|
140
152
|
4. Publish the AVIF output:
|
|
141
153
|
|
|
@@ -144,17 +156,18 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
144
156
|
--base-url https://img.example.com --urls > urls.json
|
|
145
157
|
```
|
|
146
158
|
|
|
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.
|
|
159
|
+
`publish` requires at least one `.avif` in the source unless you pass `--allow-empty --resync`; it syncs only `.avif` files and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync. It lists every `.avif` under OUT, not just files changed in this run, and is not filtered by `[publish].exclude` or `--exclude`; excluded files can still appear in it. If you rely on excludes to hide files, do not publish the manifest until this behavior is fixed.
|
|
148
160
|
|
|
149
161
|
5. Re-run later:
|
|
150
162
|
|
|
151
163
|
```bash
|
|
152
164
|
pix shrink -s ~/Pictures -d ~/Pictures-avif
|
|
153
|
-
pix
|
|
154
|
-
pix publish -s ~/Pictures-avif -d r2:images
|
|
165
|
+
pix mirror -s ~/Pictures -d onedrive:photos
|
|
166
|
+
pix publish -s ~/Pictures-avif -d r2:images \
|
|
167
|
+
--base-url https://img.example.com --urls
|
|
155
168
|
```
|
|
156
169
|
|
|
157
|
-
Only changes are encoded and transferred.
|
|
170
|
+
Only changes are encoded and transferred. `--base-url` is not persisted between runs; set `[publish].base_url` in config to avoid repeating it.
|
|
158
171
|
|
|
159
172
|
## Command cheat sheet
|
|
160
173
|
|
|
@@ -162,9 +175,10 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
162
175
|
|---|---|
|
|
163
176
|
| Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
|
|
164
177
|
| 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
|
|
167
|
-
| First
|
|
178
|
+
| Remove tracked outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
|
|
179
|
+
| Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
|
|
180
|
+
| First sync (new or empty destination) | `pix mirror -s SRC -d REMOTE` |
|
|
181
|
+
| Rebuild the baseline (non-empty destination, or after changing include/exclude) | `pix mirror -s SRC -d REMOTE --resync` |
|
|
168
182
|
| Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
|
|
169
183
|
| Diagnose the environment | `pix doctor` |
|
|
170
184
|
| Generate a config file | `pix config init` |
|
|
@@ -174,19 +188,21 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
|
|
|
174
188
|
| Situation | What happens | Protection |
|
|
175
189
|
|---|---|---|
|
|
176
190
|
| Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
|
|
177
|
-
| First sync |
|
|
178
|
-
|
|
|
179
|
-
|
|
|
191
|
+
| First sync (destination empty) | `pix` creates the destination and initializes the baseline | Preview with `--dry-run`; it may create an empty destination but does not transfer or delete. No manual rclone needed |
|
|
192
|
+
| Destination has matching content but no baseline | `pix` does not auto-resync; bisync fails instead of overwriting | Confirm which side is authoritative, then add `--resync` |
|
|
193
|
+
| Include/exclude filters changed | When filters are active, rclone detects the changed filters file and exits 7 | Run with `--resync` after changing `include` or `exclude`; `--dry-run --resync` previews but does not write the baseline |
|
|
194
|
+
| Non-image files in the source | `mirror` ignores them by default; `publish` ignores anything that is not `.avif`. If no file matches, the source counts as empty and is refused | Add extensions to `[mirror].include` if they should be mirrored; use `--allow-empty --resync` only for an intentional empty sync |
|
|
195
|
+
| Empty source | `mirror` / `publish` refuse. `shrink --prune` refuses only while state still has entries; empty state is a no-op | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
|
|
180
196
|
| `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
|
|
181
197
|
| Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
|
|
182
|
-
| State
|
|
198
|
+
| State missing or corrupt | Full rebuild; corrupt state logs a warning | Only causes re-encoding; outputs stay valid |
|
|
183
199
|
|
|
184
200
|
## Output and exit codes
|
|
185
201
|
|
|
186
202
|
- Logs and progress go to stderr.
|
|
187
|
-
- stdout carries `doctor` reports,
|
|
203
|
+
- stdout carries `doctor` reports, `shrink --dry-run` plans, `config init`'s `wrote <path>`, and `--urls` JSON. `mirror` / `publish --dry-run` output comes from rclone.
|
|
188
204
|
- Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
|
|
189
|
-
- `
|
|
205
|
+
- `mirror` / `publish` return rclone's bisync exit code unchanged; destination preflight failures return 1.
|
|
190
206
|
|
|
191
207
|
URL manifest example:
|
|
192
208
|
|
|
@@ -202,9 +218,9 @@ URL manifest example:
|
|
|
202
218
|
|---|---|
|
|
203
219
|
| `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
220
|
| rclone is older than 1.66 | Upgrade rclone |
|
|
205
|
-
| bisync reports a missing baseline |
|
|
221
|
+
| bisync reports a missing baseline or changed filters file | The destination has matching content, or the include/exclude rules changed (including the first run after upgrading). Confirm local should win, then add `--resync`; an empty destination initializes automatically. `--dry-run --resync` previews but does not write the baseline. |
|
|
206
222
|
| `OUT/.pix/lock` already exists | Check for another `shrink`, then remove a stale lock |
|
|
207
|
-
| Configuration fails to parse |
|
|
223
|
+
| Configuration fails to parse | The error names the file and location; fix the TOML or remove the file to fall back to defaults. `pix doctor` cannot run until the config parses |
|
|
208
224
|
| `publish` says the source has no `.avif` | Run `pix shrink` first |
|
|
209
225
|
| URL-related error | `base_url` must start with `http://` or `https://`, and `--urls` requires it |
|
|
210
226
|
|