Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- feat: `--skip-created-before` to limit assets by creation date [#466](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/466) [#1111](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1111)
- bug: `--watch-with-interval` does not process updates from iCloud [#1144](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1144) [#1142](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1142)

## 1.27.5 (2025-05-08)

Expand Down
44 changes: 22 additions & 22 deletions src/icloudpd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1294,36 +1294,36 @@ def core(
logger.info("Authentication completed successfully")
return 0

download_photo = downloader(icloud)

# Access to the selected library. Defaults to the primary photos object.
library_object: PhotoLibrary = icloud.photos

if list_libraries:
library_names = (
icloud.photos.private_libraries.keys() | icloud.photos.shared_libraries.keys()
)
print(*library_names, sep="\n")

else:
download_photo = downloader(icloud)

# After 6 or 7 runs within 1h Apple blocks the API for some time. In that
# case exit.
try:
# Access to the selected library. Defaults to the primary photos object.
library_object: PhotoLibrary = icloud.photos
if library:
if library in icloud.photos.private_libraries:
library_object = icloud.photos.private_libraries[library]
elif library in icloud.photos.shared_libraries:
library_object = icloud.photos.shared_libraries[library]
else:
logger.error("Unknown library: %s", library)
return 1
except PyiCloudAPIResponseException as err:
# For later: come up with a nicer message to the user. For now take the
# exception text
logger.error("error?? %s", err)
return 1

while True:
# After 6 or 7 runs within 1h Apple blocks the API for some time. In that
# case exit.
try:
if library:
if library in icloud.photos.private_libraries:
library_object = icloud.photos.private_libraries[library]
elif library in icloud.photos.shared_libraries:
library_object = icloud.photos.shared_libraries[library]
else:
logger.error("Unknown library: %s", library)
return 1
photos = library_object.albums[album] if album else library_object.all
except PyiCloudAPIResponseException as err:
# For later: come up with a nicer message to the user. For now take the
# exception text
logger.error("error?? %s", err)
return 1
photos = library_object.albums[album] if album else library_object.all

if list_albums:
print("Albums:")
Expand Down
98 changes: 42 additions & 56 deletions src/pyicloud_ipd/services/photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,6 @@ def __init__(self, service: "PhotosService", zone_id: Dict[str, Any], library_ty
self.library_type = library_type
self.service_endpoint = self.service.get_service_endpoint(library_type)

self._albums: Optional[Dict[str, PhotoAlbum]] = None
self._all: Optional[PhotoAlbum] = None
self._recently_deleted: Optional[PhotoAlbum] = None

url = ('%s/records/query?%s' %
(self.service_endpoint, urlencode(self.service.params)))
json_data = json.dumps({
Expand All @@ -186,41 +182,40 @@ def __init__(self, service: "PhotosService", zone_id: Dict[str, Any], library_ty

@property
def albums(self) -> Dict[str, "PhotoAlbum"]:
if not self._albums:
self._albums = {
albums = {
name: PhotoAlbum(self.service, self.service_endpoint, name, zone_id=self.zone_id, **props) # type: ignore[arg-type] # dynamically builing params
for (name, props) in self.SMART_FOLDERS.items()
}

for folder in self._fetch_folders():
# FIXME: Handle subfolders
if folder['recordName'] in ('----Root-Folder----',
'----Project-Root-Folder----') or \
(folder['fields'].get('isDeleted') and
folder['fields']['isDeleted']['value']):
continue
for folder in self._fetch_folders():
# FIXME: Handle subfolders
if folder['recordName'] in ('----Root-Folder----',
'----Project-Root-Folder----') or \
(folder['fields'].get('isDeleted') and
folder['fields']['isDeleted']['value']):
continue

folder_id = folder['recordName']
folder_obj_type = \
"CPLContainerRelationNotDeletedByAssetDate:%s" % folder_id
folder_name = base64.b64decode(
folder['fields']['albumNameEnc']['value']).decode('utf-8')
query_filter = [{
"fieldName": "parentId",
"comparator": "EQUALS",
"fieldValue": {
"type": "STRING",
"value": folder_id
}
}]

album = PhotoAlbum(self.service, self.service_endpoint, folder_name,
'CPLContainerRelationLiveByAssetDate',
folder_obj_type, 'ASCENDING', query_filter,
zone_id=self.zone_id)
albums[folder_name] = album

folder_id = folder['recordName']
folder_obj_type = \
"CPLContainerRelationNotDeletedByAssetDate:%s" % folder_id
folder_name = base64.b64decode(
folder['fields']['albumNameEnc']['value']).decode('utf-8')
query_filter = [{
"fieldName": "parentId",
"comparator": "EQUALS",
"fieldValue": {
"type": "STRING",
"value": folder_id
}
}]

album = PhotoAlbum(self.service, self.service_endpoint, folder_name,
'CPLContainerRelationLiveByAssetDate',
folder_obj_type, 'ASCENDING', query_filter,
zone_id=self.zone_id)
self._albums[folder_name] = album

return self._albums
return albums

def _fetch_folders(self) -> Sequence[Dict[str, Any]]:
if self.library_type == "shared":
Expand All @@ -243,15 +238,11 @@ def _fetch_folders(self) -> Sequence[Dict[str, Any]]:

@property
def all(self) -> "PhotoAlbum":
if not self._all:
self._all = PhotoAlbum(self.service, self.service_endpoint, "", zone_id=self.zone_id, **self.WHOLE_COLLECTION) # type: ignore[arg-type] # dynamically builing params
return self._all
return PhotoAlbum(self.service, self.service_endpoint, "", zone_id=self.zone_id, **self.WHOLE_COLLECTION) # type: ignore[arg-type] # dynamically builing params

@property
def recently_deleted(self) -> "PhotoAlbum":
if not self._recently_deleted:
self._recently_deleted = PhotoAlbum(self.service, self.service_endpoint, "", zone_id=self.zone_id, **self.RECENTLY_DELETED) # type: ignore[arg-type] # dynamically builing params
return self._recently_deleted
return PhotoAlbum(self.service, self.service_endpoint, "", zone_id=self.zone_id, **self.RECENTLY_DELETED) # type: ignore[arg-type] # dynamically builing params

class PhotosService(PhotoLibrary):
"""The 'Photos' iCloud service.
Expand Down Expand Up @@ -357,8 +348,6 @@ def __init__(self, service:PhotosService, service_endpoint: str, name: str, list
self.page_size = page_size
self.exception_handler: Optional[Callable[[Exception, int], None]] = None

self._len: Optional[int] = None

if zone_id:
self._zone_id: Dict[str, Any] = zone_id
else:
Expand All @@ -372,21 +361,18 @@ def __iter__(self) -> Generator["PhotoAsset", Any, None]:
return self.photos

def __len__(self) -> int:
if self._len is None:
url = ('%s/internal/records/query/batch?%s' %
(self.service_endpoint,
urlencode(self.service.params)))
request = self.service.session.post(
url,
data=json.dumps(self._count_query_gen(self.obj_type)),
headers={'Content-type': 'text/plain'}
)
response = request.json()

self._len = (response["batch"][0]["records"][0]["fields"]
["itemCount"]["value"])
url = ('%s/internal/records/query/batch?%s' %
(self.service_endpoint,
urlencode(self.service.params)))
request = self.service.session.post(
url,
data=json.dumps(self._count_query_gen(self.obj_type)),
headers={'Content-type': 'text/plain'}
)
response = request.json()

return self._len
return int(response["batch"][0]["records"][0]["fields"]
["itemCount"]["value"])

# Perform the request in a separate method so that we
# can mock it to test session errors.
Expand Down
Loading