package main import ( "encoding/json" "encoding/xml" "fmt" "io" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" "gitea.k3s.k0.nu/tools/photocli/internal/manifest" "gitea.k3s.k0.nu/tools/photocli/internal/photos" ) var ( progressPollInterval = 100 * time.Millisecond exportTimeout = 2 * time.Second configValues map[string]string configLoaded bool mkdirTempFunc = os.MkdirTemp createTempFunc = os.CreateTemp writeFileFunc = os.WriteFile readFileFunc = os.ReadFile statFunc = os.Stat renameFunc = os.Rename openFileFunc = os.OpenFile removeFunc = os.Remove ) type exportOptions struct { dryRun bool retry int onlyFavorites bool media string jsonOut bool verify bool format string sidecar string metadataOnly bool reverseGeocode bool minSize int64 maxSize int64 dateTemplate string } type commandSummary struct { Exported int `json:"exported"` Failed int `json:"failed"` Total int `json:"total"` } const ( exitOK = 0 exitErr = 1 exitPartial = 2 exitAuth = 3 ) func run(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int { if len(args) < 1 { usage(stderr) return exitErr } cmd := args[0] switch cmd { case "albums": return cmdAlbums(stdout, stderr, bridge) case "photos": return cmdPhotos(args[1:], stdout, stderr, bridge) case "tree": return cmdTree(stdout, stderr, bridge) case "backup-all": return cmdBackupAll(args[1:], stdout, stderr, bridge) case "export": return cmdExport(args[1:], stdout, stderr, bridge) case "report": return cmdReport(args[1:], stdout, stderr) case "diff": return cmdDiff(args[1:], stdout, stderr, bridge) case "verify": return cmdVerify(args[1:], stdout, stderr) case "retry-failed": return cmdRetryFailed(args[1:], stdout, stderr, bridge) case "failures": return cmdFailures(args[1:], stdout, stderr) case "status": return cmdStatus(args[1:], stdout, stderr) case "version", "--version", "-v": fmt.Fprintln(stdout, version) return exitOK case "help", "--help", "-h": usage(stderr) return exitOK default: fmt.Fprintf(stderr, "unknown command: %s\n", cmd) usage(stderr) return exitErr } } func usage(w io.Writer) { fmt.Fprintln(w, `photoscli - export and verify Apple Photos backups DESCRIPTION photoscli is a macOS-only Apple Photos exporter. It uses PhotoKit through a small Objective-C bridge to list albums, inspect assets, export previews or originals, keep resumable manifests, log structured export events, and verify backup integrity. Prebuilt releases target Apple Silicon Macs only (darwin/arm64: M1/M2/M3/M4 or newer). Intel Macs are not currently a supported release target. The tool is intended for repeatable backups. By default it records exported asset IDs in a manifest so later runs can skip work already completed. USAGE photoscli albums photoscli photos --album-id photoscli tree photoscli export --album-id --out [flags] photoscli backup-all --out [flags] photoscli report --out [--manifest jsonl|sqlite] photoscli diff --album-id --out [--manifest jsonl|sqlite] photoscli verify --out [--manifest jsonl|sqlite] photoscli retry-failed --out photoscli retry-failed --out --clear-on-success photoscli failures list --out photoscli failures clear --out photoscli status --out [--json] photoscli version photoscli help COMMANDS albums Request Photos access and list user-created albums as: photos --album-id List assets in one album. The album can be a PhotoKit local identifier or an exact album title. Output includes asset ID, filename, cloud state, media type, dimensions, optional creation date, optional duration, and a trailing * for favorites. tree Print the Photos folder/album hierarchy as an indented tree. export --album-id --out [flags] Export assets from one album. Preview JPEGs are exported by default. Use --originals to export original files instead. backup-all --out [flags] Walk the Photos tree and export every album into a matching directory structure. Duplicate sibling album names are disambiguated with album IDs. report --out [--manifest jsonl|sqlite] Print manifest entry count and failure count. diff --album-id --out [--manifest jsonl|sqlite] Compare album assets against the manifest. Missing assets are printed as . Exits 2 when differences are found. verify --out [--manifest jsonl|sqlite] Verify that manifest entries point to files that exist on disk. Missing files are printed as . Exits 2 on missing files. Add --sidecar to verify expected XMP sidecars too. retry-failed --out Retry assets previously written to failures.jsonl. failures list|clear --out List or clear deduplicated failure records. status --out [--manifest jsonl|sqlite] [--json] Show manifest type, entry count, and failure count for a backup. COMMON EXPORT FLAGS --out Destination directory. Required for export, backup-all, report, diff, verify, and retry-failed. --album-id Album PhotoKit local identifier or exact album title. Required by export, photos, and diff. --size Target longest-side preview size in pixels. Default: 1024. Ignored when --originals is used. --quality <1-100> JPEG preview compression quality. Default: 85. Ignored when --originals is used. --originals Export original files instead of preview JPEGs. --concurrency Number of parallel export workers. Default: 3. Values above the bridge's progress slot count are capped automatically. --retry Retry failed exports N times with a small backoff. Default: 0. --dry-run Print assets that would be exported without writing files, manifests, or logs. Useful before large backup-all runs. --json Print a machine-readable summary to stdout: {"exported":N,"failed":N,"total":N} --verify Run manifest/file verification after export or backup-all. --sidecar none|xmp Write opt-in XMP sidecar metadata next to each exported file. Default: none. If XMP writing fails, the asset is counted as failed. --metadata-only With --sidecar xmp, write or refresh XMP sidecars for files already in the manifest without exporting media files. Requires a manifest. --reverse-geocode With --sidecar xmp, use Apple MapKit on macOS 26+ to add address metadata for assets with GPS coordinates. Results are cached under .photoscli. FILTERING AND SELECTION --since Include only assets on or after a date. Accepts YYYY-MM-DD or RFC3339. --sort oldest|newest Sort assets by creation date. Default: oldest. --only-favorites Export only favorite assets. --media photos|videos|all Select media type. Default: photos. --include-videos Compatibility shortcut that includes videos/audio. --exclude-album backup-all only. Repeatable. Excludes album names by exact match or glob pattern, for example --exclude-album "Temp*". --min-size Include only assets with estimated pixel count >= n. --max-size Include only assets with estimated pixel count <= n. OUTPUT LAYOUT AND FORMAT --date-template