210 lines
6.9 KiB
Markdown
210 lines
6.9 KiB
Markdown
# photoscli
|
|
|
|
`photoscli` is a small macOS-only CLI written in Go that reads data from Apple Photos through a PhotoKit bridge.
|
|
|
|
It supports five tasks:
|
|
|
|
- listing albums
|
|
- listing asset IDs, filenames, and cloud status in an album
|
|
- showing the folder and album tree
|
|
- backing up all albums into the Photos folder tree
|
|
- exporting resized JPEG previews or original files from an album
|
|
|
|
## What The Code Does
|
|
|
|
The executable lives in `cmd/photoscli` and calls into `internal/photos`, which wraps an Objective-C bridge in `bridge/`.
|
|
|
|
Current behavior:
|
|
|
|
- `albums` prints one line per album as `<album-id>\t<title>`
|
|
- `photos --album-id <id-or-title>` prints one asset per line as `<id>\t<filename>\t<cloud>`; accepts either a PhotoKit local identifier or an album title
|
|
- `tree` prints the human-readable folder and album hierarchy from Apple Photos
|
|
- `backup-all --out <dir> [--size <px>] [--originals]` exports every album into a matching folder tree, showing per-asset progress with filename, size, and cloud status
|
|
- `export --album-id <id-or-title> --out <dir> [--size <px>] [--originals]` exports either JPEG previews or original files with a progress bar showing filename, size, and cloud status; `--album-id` accepts either a PhotoKit local identifier or an album title
|
|
|
|
The bridge uses PhotoKit to:
|
|
|
|
- request access to the user's Photos library
|
|
- fetch album collections by local identifier or album title
|
|
- fetch album assets sorted by `creationDate` ascending
|
|
- render resized images with `PHImageManager`
|
|
- write JPEG files with compression `0.85`
|
|
- support graceful cancellation via a cancel flag checked between file exports
|
|
|
|
## Requirements
|
|
|
|
- macOS
|
|
- Go 1.22+
|
|
- Xcode command-line tools
|
|
|
|
The project builds with cgo and links against `Photos`, `Foundation`, and `AppKit`.
|
|
|
|
## Build
|
|
|
|
```bash
|
|
make build
|
|
```
|
|
|
|
Output binary:
|
|
|
|
```bash
|
|
./bin/photoscli
|
|
```
|
|
|
|
## Test
|
|
|
|
```bash
|
|
make test
|
|
```
|
|
|
|
Tests run against a stub bridge, so they do not require real Photos access.
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
./bin/photoscli albums
|
|
./bin/photoscli photos --album-id "<album-local-identifier>"
|
|
./bin/photoscli photos --album-id "Vacation"
|
|
./bin/photoscli tree
|
|
./bin/photoscli backup-all --out ./backup
|
|
./bin/photoscli backup-all --out ./backup --originals
|
|
./bin/photoscli export --album-id "<album-local-identifier>" --out ./export
|
|
./bin/photoscli export --album-id "Vacation" --out ./export
|
|
./bin/photoscli export --album-id "<album-local-identifier>" --out ./export --size 2048
|
|
./bin/photoscli export --album-id "<album-local-identifier>" --out ./export --originals
|
|
```
|
|
|
|
### Commands
|
|
|
|
`albums`
|
|
|
|
- Requests Photos access
|
|
- Lists albums as tab-separated album ID and title
|
|
|
|
Example output:
|
|
|
|
```text
|
|
5E9F.../L0/001 Vacation
|
|
8A1B.../L0/001 Work
|
|
```
|
|
|
|
`photos --album-id <id-or-title>`
|
|
|
|
- Requests Photos access
|
|
- If the value looks like a PhotoKit local identifier, uses it directly
|
|
- Otherwise searches album titles for a match and resolves the identifier
|
|
- Lists asset local identifiers and cloud status for the given album
|
|
|
|
Example output:
|
|
|
|
```text
|
|
1F2A.../L0/001 IMG_0001.JPG local
|
|
9C4D.../L0/001 IMG_0002.JPG cloud
|
|
```
|
|
|
|
`backup-all --out <dir> [--size <px>] [--originals]`
|
|
|
|
- Requests Photos access
|
|
- Walks the Photos folder and album hierarchy
|
|
- Creates directories as `out/folder/album/files`
|
|
- Exports previews by default, originals when `--originals` is present
|
|
- Shows per-asset progress bar with filename, file size, and cloud/local status
|
|
- Uses `--size` only for preview export
|
|
|
|
Example layout:
|
|
|
|
```text
|
|
backup/
|
|
Trips/
|
|
Italy 2024/
|
|
Venice/
|
|
0000_....jpg
|
|
Favorites/
|
|
0000_....jpg
|
|
```
|
|
|
|
`export --album-id <id-or-title> --out <dir> [--size <px>] [--originals]`
|
|
|
|
- Requests Photos access
|
|
- Resolves `--album-id` by local identifier first, then by album title if not found
|
|
- Creates the output directory if needed
|
|
- Exports assets one at a time with a progress bar: `[=======---] 50% filename.jpg 1.2 MB cloud`
|
|
- Shows file size and cloud/local status for each exported asset
|
|
- Exports resized JPEG previews by default
|
|
- Exports original files when `--originals` is present
|
|
- Writes a summary like `exported 10 photos to ./export` or `exported 10 original files to ./export` to stderr
|
|
|
|
`--size` is the target bounding box passed to PhotoKit for preview export. Default: `1024`.
|
|
|
|
`--originals` switches export mode to original-file export. In that mode, `--size` is ignored.
|
|
|
|
`tree`
|
|
|
|
- Requests Photos access
|
|
- Prints folders and albums as an indented tree
|
|
- Omits internal album IDs for human-readable output
|
|
|
|
Example output:
|
|
|
|
```text
|
|
Trips
|
|
Italy 2024
|
|
Venice
|
|
Favorites
|
|
```
|
|
|
|
## Permissions
|
|
|
|
On first use, macOS may prompt for Photos access.
|
|
|
|
If access is denied, the CLI returns an error telling you to grant access in:
|
|
|
|
`System Settings > Privacy & Security`
|
|
|
|
## Export Details
|
|
|
|
Exported files currently:
|
|
|
|
- are JPEGs when exporting previews
|
|
- keep their original filenames when exporting originals when possible
|
|
- fall back to a sanitized asset identifier if an original filename is unavailable
|
|
- prefix duplicate original filenames with the asset index to avoid collisions
|
|
- name preview exports like `0000_<asset-local-identifier>.jpg`
|
|
- replace `/` and `\` in asset IDs with `_` for generated preview filenames
|
|
- replace `/` and `\` in folder and album names with `_` when creating backup directory names
|
|
- preserve ordering based on ascending asset creation date
|
|
|
|
If some assets fail but at least one succeeds, the command still succeeds and reports the number exported.
|
|
|
|
If all exports fail, the command returns an error.
|
|
|
|
## Signal Handling
|
|
|
|
Sending `SIGINT` (Ctrl+C) or `SIGTERM` during export or backup triggers a graceful shutdown:
|
|
|
|
1. The CLI prints `received signal, finishing current file...` to stderr
|
|
2. The current file export is allowed to complete
|
|
3. No further files are started
|
|
4. The process exits after the in-progress file finishes
|
|
|
|
A second signal forces an immediate exit.
|
|
|
|
## Architecture
|
|
|
|
- `cmd/photoscli`: CLI entrypoint, argument parsing, and album name resolution
|
|
- `internal/photos`: Go bridge interface, JSON parsing, and error mapping
|
|
- `bridge/`: Objective-C PhotoKit implementation plus a C test stub
|
|
|
|
Data passed from Objective-C to Go is serialized as JSON and unmarshaled into Go structs.
|
|
|
|
## Known Limitations
|
|
|
|
- The tree view only shows user collections exposed through PhotoKit's top-level user collections API
|
|
- Album title resolution matches the first album with that title; if multiple albums share a title, use the local identifier instead
|
|
- `photos` only prints asset IDs and filenames, not dates or metadata
|
|
- Preview export uses PhotoKit preview rendering, not original file export
|
|
- Original export currently writes the first PhotoKit asset resource for each asset, which may not capture every related representation for complex assets
|
|
- iCloud-backed assets may require network download during export
|
|
- A second interrupt signal forces an immediate exit without waiting for the current file
|
|
- Partial export failures are not listed individually
|