initial commit: applephotos CLI with progress, cloud status, per-asset export
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# applephotos
|
||||
|
||||
`applephotos` 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 and filenames 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/applephotos` 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 local identifier and filename per line; 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 under the output directory
|
||||
- `export --album-id <id-or-title> --out <dir> [--size <px>] [--originals]` exports either JPEG previews or original files and reports the number exported on stderr; `--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/applephotos
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
Tests run against a stub bridge, so they do not require real Photos access.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
./bin/applephotos albums
|
||||
./bin/applephotos photos --album-id "<album-local-identifier>"
|
||||
./bin/applephotos photos --album-id "Vacation"
|
||||
./bin/applephotos tree
|
||||
./bin/applephotos backup-all --out ./backup
|
||||
./bin/applephotos backup-all --out ./backup --originals
|
||||
./bin/applephotos export --album-id "<album-local-identifier>" --out ./export
|
||||
./bin/applephotos export --album-id "Vacation" --out ./export
|
||||
./bin/applephotos export --album-id "<album-local-identifier>" --out ./export --size 2048
|
||||
./bin/applephotos 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 for the given album
|
||||
|
||||
Example output:
|
||||
|
||||
```text
|
||||
1F2A.../L0/001 IMG_0001.JPG
|
||||
9C4D.../L0/001 IMG_0002.JPG
|
||||
```
|
||||
|
||||
`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
|
||||
- Exports original files when `--originals` is present
|
||||
- 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 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.
|
||||
|
||||
- `cmd/applephotos`: 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
|
||||
- Export is synchronous and has no progress output
|
||||
- A second interrupt signal forces an immediate exit without waiting for the current file
|
||||
- Partial export failures are not listed individually
|
||||
Reference in New Issue
Block a user