Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 162 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ For support and discussions, join our Discord community: [Join our Discord commu
Install the library and CLI with:

```console
$ pip install pyicloud
pip install pyicloud
```

This installs the `icloud` command line interface alongside the Python package.
Expand Down Expand Up @@ -81,8 +81,8 @@ subcommands such as `auth`, `account`, `devices`, `calendar`,
Command options belong on the final command that uses them. For example:

```console
$ icloud auth login --username jappleseed@apple.com
$ icloud account summary --format json
icloud auth login --username jappleseed@apple.com
icloud account summary --format json
```

The root command only exposes help and shell-completion utilities.
Expand All @@ -91,7 +91,7 @@ You can store your password in the system keyring using the
command-line tool:

```console
$ icloud auth login --username jappleseed@apple.com
icloud auth login --username jappleseed@apple.com
Enter iCloud password for jappleseed@apple.com:
Save password in keyring? (y/N)
```
Expand All @@ -107,34 +107,42 @@ api = PyiCloudService('jappleseed@apple.com')
CLI examples:

```console
$ icloud auth status
$ icloud auth login --username jappleseed@apple.com
$ icloud auth login --username jappleseed@apple.com --china-mainland
$ icloud auth login --username jappleseed@apple.com --accept-terms
$ icloud account summary
$ icloud account summary --format json
$ icloud devices list --locate
$ icloud devices list --with-family
$ icloud devices show "Example iPhone"
$ icloud devices export "Example iPhone" --output ./iphone.json
$ icloud calendar events --username jappleseed@apple.com --period week
$ icloud contacts me --username jappleseed@apple.com
$ icloud drive list /Documents --username jappleseed@apple.com
$ icloud photos albums --username jappleseed@apple.com
$ icloud hidemyemail list --username jappleseed@apple.com
$ icloud auth logout
$ icloud auth logout --keep-trusted
$ icloud auth logout --all-sessions
$ icloud auth logout --keep-trusted --all-sessions
$ icloud auth logout --remove-keyring
$ icloud auth keyring delete --username jappleseed@apple.com
icloud auth status
icloud auth login --username jappleseed@apple.com
icloud auth login --username jappleseed@apple.com --china-mainland
icloud auth login --username jappleseed@apple.com --accept-terms
icloud account summary
icloud account summary --format json
icloud devices list --locate
icloud devices list --with-family
icloud devices show "Example iPhone"
icloud devices export "Example iPhone" --output ./iphone.json
icloud calendar events --username jappleseed@apple.com --period week
icloud contacts me --username jappleseed@apple.com
icloud drive list /Documents --username jappleseed@apple.com
icloud photos libraries --username jappleseed@apple.com
icloud photos albums --username jappleseed@apple.com
icloud photos list --album Screenshots --limit 20 --username jappleseed@apple.com
icloud photos get photo-id-123 --format json --username jappleseed@apple.com
icloud photos sync --directory ./downloads --username jappleseed@apple.com
icloud photos watch --directory ./downloads --recent 1 --interval 300 --username jappleseed@apple.com
icloud photos sync --directory ./downloads --album Favorites --folder-structure '{:%Y/%m}' --username jappleseed@apple.com
icloud photos sync-cursor --username jappleseed@apple.com
icloud photos changes --since '<sync-cursor>' --username jappleseed@apple.com
icloud hidemyemail list --username jappleseed@apple.com
icloud auth logout
icloud auth logout --keep-trusted
icloud auth logout --all-sessions
icloud auth logout --keep-trusted --all-sessions
icloud auth logout --remove-keyring
icloud auth keyring delete --username jappleseed@apple.com
```

If you would like to delete a password stored in your system keyring,
use the dedicated keyring subcommand:

```console
$ icloud auth keyring delete --username jappleseed@apple.com
icloud auth keyring delete --username jappleseed@apple.com
```

The `auth` command group lets you inspect and manage persisted sessions:
Expand Down Expand Up @@ -757,6 +765,112 @@ You can interact with the `trash` similar to a standard directory, with some res

You can access the iCloud Photo Library through the `photos` property.

### Photos CLI

The Photos CLI is split into browse commands and sync commands:

- `icloud photos libraries`, `albums`, `list`, `get`, `changes`, and `sync-cursor`
are read-focused inspection commands.
- `icloud photos sync` and `icloud photos watch` are the modern replacement path
for `icloud_photos_downloader`.

The CloudKit-backed browse/sync path targets the private iCloud Photos library
and Shared Library zones discovered as `shared:<zoneName>` keys. Legacy Shared
Albums / shared streams remain available through the separate shared-stream
adapter.

Current scope:

- private-library browsing, download, sync, watch, and mutation flows use the
modern CloudKit-backed Photos service
- Shared Library CloudKit reads are exposed through `photos libraries` as
`shared:<zoneName>` keys and are supported by `list`, `get`, `download`,
`changes`, `sync-cursor`, `sync`, and `watch`
- Shared Library album filters are currently limited to the captured, tested
smart albums `Library` and `Favorites`
- Shared Albums / shared streams continue to use the legacy shared-stream
adapter under the `shared` library key
- Shared Library album-scoped browsing and mixed `Both Libraries` semantics are
still narrower than the private-library path and continue to rely on further
captures

Support matrix:

- Private library `root`: full browse/download/sync/watch surface plus the
implemented private-library mutations
- Shared Library `shared:<zoneName>`: library-scoped reads plus `Library` /
`Favorites` album filters, `sync-cursor`, `sync`, `watch`, and
favorite/unfavorite mutations
- Legacy Shared Albums `shared`: old shared-stream adapter only, not a valid
CloudKit browse/sync target

Typical browse and sync examples:

```console
icloud photos libraries --username jappleseed@apple.com
icloud photos albums --username jappleseed@apple.com
icloud photos list --album Screenshots --limit 20 --username jappleseed@apple.com
icloud photos list --library 'shared:<zoneName>' --limit 20 --username jappleseed@apple.com
icloud photos list --library 'shared:<zoneName>' --album Favorites --limit 20 --username jappleseed@apple.com
icloud photos get photo-id-123 --format json --username jappleseed@apple.com
icloud photos get photo-id-123 --library 'shared:<zoneName>' --format json --username jappleseed@apple.com
icloud photos sync --directory ./downloads --recent 30 --folder-structure '{:%Y/%m}' --username jappleseed@apple.com
icloud photos sync --library 'shared:<zoneName>' --directory ./shared-downloads --username jappleseed@apple.com
icloud photos sync --directory ./downloads --album Favorites --size original --live-photo-size medium --username jappleseed@apple.com
icloud photos watch --directory ./downloads --recent 1 --interval 300 --username jappleseed@apple.com
icloud photos watch --library 'shared:<zoneName>' --directory ./shared-downloads --interval 300 --username jappleseed@apple.com
icloud photos changes --since '<sync-cursor>' --limit 100 --username jappleseed@apple.com
icloud photos changes --library 'shared:<zoneName>' --since '<sync-cursor>' --limit 100 --username jappleseed@apple.com
icloud photos sync-cursor --username jappleseed@apple.com
icloud photos sync-cursor --library 'shared:<zoneName>' --username jappleseed@apple.com
```

Library-key notes:

- `root` is the private iCloud Photos library
- `shared:<zoneName>` is a CloudKit-backed Shared Library zone
- `shared` is the legacy Shared Albums / shared-stream adapter and is not a
drop-in substitute for CloudKit library reads, `sync-cursor`, `sync`, or
`watch`

`sync` and `watch` support downloader-style options such as `--recent`,
`--until-found`, repeatable `--album`, `--folder-structure`, `--size`,
`--live-photo-size`, `--skip-videos`, `--skip-live-photos`, `--align-raw`,
`--xmp-sidecar`, `--set-exif-datetime`, `--only-print-filenames`, `--dry-run`,
`--auto-delete`, and `--keep-icloud-recent-days`.

### Migrating from `icloud_photos_downloader`

If you currently use `icloudpd`, the equivalent workflow in `pyicloud` is:

- Authenticate once with `icloud auth login`, then run `icloud photos sync` or
`icloud photos watch`.
- Use `icloud photos sync` for one-shot materialization into a local directory.
- Use `icloud photos watch` for repeated polling with the same sync options.
- For private-library downloader workflows, `sync` and `watch` are the intended
replacement path. Shared streams remain a separate surface.

Common option mappings:

- `icloudpd --directory DIR` -> `icloud photos sync --directory DIR`
- `icloudpd --recent N` -> `icloud photos sync --recent N`
- `icloudpd --until-found N` -> `icloud photos sync --until-found N`
- `icloudpd --album NAME` -> `icloud photos sync --album NAME`
- `icloudpd --folder-structure FORMAT` -> `icloud photos sync --folder-structure FORMAT`
- `icloudpd --size SIZE` -> `icloud photos sync --size SIZE`
- `icloudpd --live-photo-size SIZE` -> `icloud photos sync --live-photo-size SIZE`
- `icloudpd --skip-videos` -> `icloud photos sync --skip-videos`
- `icloudpd --skip-live-photos` -> `icloud photos sync --skip-live-photos`
- `icloudpd --align-raw MODE` -> `icloud photos sync --align-raw MODE`
- `icloudpd --xmp-sidecar` -> `icloud photos sync --xmp-sidecar`
- `icloudpd --set-exif-datetime` -> `icloud photos sync --set-exif-datetime`
- `icloudpd --auto-delete` -> `icloud photos sync --auto-delete`
- `icloudpd --only-print-filenames` -> `icloud photos sync --only-print-filenames`
- `icloudpd --watch-with-interval SECONDS` -> `icloud photos watch --interval SECONDS`

Unlike `icloudpd`, authentication and session management stay under
`icloud auth ...`; the Photos commands do not reimplement separate auth flags.

```pycon
>>> api.photos.all
<PhotoAlbum: 'All Photos'>
Expand All @@ -778,6 +892,16 @@ To delete an individual album, call the `delete` method.
True
```

Shared streams are still available separately:

```pycon
>>> api.photos.shared_streams
<AlbumContainer: ...>
```

Those shared-stream albums continue to use the legacy adapter, even though the
private-library path is now CloudKit-backed.

Which you can iterate to access the photo assets. The "All Photos"
album is sorted by `added_date` so the most recently added
photos are returned first. All other albums are sorted by
Expand Down Expand Up @@ -813,12 +937,19 @@ with open(photo.versions['thumb']['filename'], 'wb') as thumb_file:
thumb_file.write(photo.download('thumb'))
```

To upload a photo use the `upload` method, which will upload the file to the requested album
this will appear automatically in your 'ALL PHOTOS' album. This will return the uploaded
To upload a photo use the `upload` method. You can upload directly through an
album object, or use the top-level `api.photos.upload(...)` helper to target
the root library or a named album. Uploads to a specific album will also appear
automatically in your `All Photos` library. Each form returns the uploaded
PhotoAsset for further information.

```python
api.photos.albums['Screenshots'].upload(file_path)
api.photos.upload(file_path)
api.photos.upload(file_path, album="Screenshots")
```

```python
api.photos.albums["Screenshots"].upload(file_path)
```

```pycon
Expand All @@ -827,6 +958,8 @@ api.photos.albums['Screenshots'].upload(file_path)
<PhotoAlbum: 'Screenshots'>
>>> album.upload("./my_test_image.jpg")
<PhotoAsset: id=AVbLPCGkp798nTb9KZozCXtO7jdQ> my_test_image.jpg
>>> api.photos.upload("./my_test_image.jpg", album="Screenshots")
<PhotoAsset: id=AVbLPCGkp798nTb9KZozCXtO7jdQ> my_test_image.jpg
```

Note: Only limited media types are accepted. Unsupported types (e.g., PNG) will return a TYPE_UNSUPPORTED error.
Expand Down
18 changes: 14 additions & 4 deletions examples.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import json
import logging
import sys
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, List, Optional
from unittest.mock import patch
from uuid import uuid4

import click
from fido2.hid import CtapHidDevice
Expand Down Expand Up @@ -447,7 +448,13 @@ def display_hidemyemail(api: PyiCloudService) -> None:
def album_management(api: PyiCloudService) -> None:
"""Test album management functions"""

album_name = "Test Album from API"
album_name = f"{datetime.now(timezone.utc).strftime('pyicloud-live-%Y%m%d-%H%M%S')}-{uuid4().hex[:8]}"
renamed_name = f"{album_name}-renamed"
print(
"Running live photo mutation validation against the authenticated account. "
"This example creates a disposable album, optionally uploads a sample file, "
"then deletes the uploaded photo and album."
)
print(f"Creating album '{album_name}'...")
album: PhotoAlbum | None = api.photos.create_album(album_name)
print(f"Album created: {album}")
Expand All @@ -456,12 +463,15 @@ def album_management(api: PyiCloudService) -> None:
return

print(f"Album '{album_name}' created successfully.")
album.name = "Renamed Album"
album.rename(renamed_name)
print(f"Album renamed to '{album.name}'")

sample_photo: Path = Path(__file__).with_name("sample.jpg")
if sample_photo.exists():
photo: PhotoAsset | None = album.upload(str(sample_photo))
photo: PhotoAsset | None = api.photos.upload(
str(sample_photo),
album=album.name,
)
if photo:
print(f"Photo uploaded successfully: {photo.filename} ({photo.item_type})")
if photo.delete():
Expand Down
Loading