initial commit: applephotos CLI with progress, cloud status, per-asset export

This commit is contained in:
Ein Anderssono
2026-06-11 20:25:07 +02:00
commit 6ec16f3966
21 changed files with 3488 additions and 0 deletions
+206
View File
@@ -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