diff --git a/README.md b/README.md index ce1fdf9a9..cdb3ec8f4 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,11 @@ photos: all_albums: false # Optional, default false. If true preserve album structure. If same photo is in multpile albums creates duplicates on filesystem folder_format: "%Y/%m" # optional, if set put photos in subfolders according to format. Format cheatsheet - https://strftime.org filters: + # List of libraries to download. If omitted (default), photos from all libraries (own and shared) are downloaded. If included, photos only + # from the listed libraries are downloaded. + # libraries: + # - PrimarySync # Name of the own library + # if all_albums is false - albums list is used as filter-in, if all_albums is true - albums list is used as filter-out # if albums list is empty and all_albums is false download all photos to "all" folder. if empty and all_albums is true download all folders albums: diff --git a/config.yaml b/config.yaml index 290d12289..c796a09f3 100644 --- a/config.yaml +++ b/config.yaml @@ -48,6 +48,11 @@ photos: all_albums: false # Optional, default false. If true preserve album structure. If same photo is in multpile albums creates duplicates on filesystem # folder_format: "%Y/%m" # optional, if set put photos in subfolders according to format. Format cheatsheet - https://strftime.org filters: + # List of libraries to download. If omitted (default), photos from all libraries (own and shared) are downloaded. If included, photos only + # from the listed libraries are downloaded. + # libraries: + # - PrimarySync # Name of the own library + # if all_albums is false - albums list is used as filter-in, if all_albums is true - albums list is used as filter-out # if albums list is empty and all_albums is false download all photos to "all" folder. if empty and all_albums is true download all folders albums: diff --git a/src/config_parser.py b/src/config_parser.py index 3de281311..e24a65ae4 100644 --- a/src/config_parser.py +++ b/src/config_parser.py @@ -280,59 +280,86 @@ def get_photos_remove_obsolete(config): def get_photos_filters(config): """Return photos filters from config.""" - photos_filters = {"albums": None, "file_sizes": ["original"], "extensions": None} + photos_filters = { + "libraries": None, + "albums": None, + "file_sizes": ["original"], + "extensions": None, + } valid_file_sizes = ["original", "medium", "thumb"] config_path = ["photos", "filters"] + + # Check for filters if not traverse_config_path(config=config, config_path=config_path): LOGGER.warning( - f"{config_path_to_string(config_path=config_path)} not found. Downloading all albums with original size ..." + f"{config_path_to_string(config_path=config_path)} not found. \ + Downloading all libraries and albums with original size ..." + ) + return photos_filters + + # Parse libraries + config_path.append("libraries") + if ( + not traverse_config_path(config=config, config_path=config_path) + or not get_config_value(config=config, config_path=config_path) + or len(get_config_value(config=config, config_path=config_path)) == 0 + ): + LOGGER.warning( + f"{config_path_to_string(config_path=config_path)} not found. Downloading all libraries ..." ) else: - config_path.append("albums") - if ( - not traverse_config_path(config=config, config_path=config_path) - or not get_config_value(config=config, config_path=config_path) - or len(get_config_value(config=config, config_path=config_path)) == 0 - ): - LOGGER.warning( - f"{config_path_to_string(config_path=config_path)} not found. Downloading all albums ..." - ) - else: - photos_filters["albums"] = get_config_value( - config=config, config_path=config_path - ) + photos_filters["libraries"] = get_config_value( + config=config, config_path=config_path + ) - config_path[2] = "file_sizes" - if not traverse_config_path(config=config, config_path=config_path): - LOGGER.warning( - f"{config_path_to_string(config_path=config_path)} not found. Downloading original size photos ..." - ) - else: - file_sizes = get_config_value(config=config, config_path=config_path) - for file_size in file_sizes: - if file_size not in valid_file_sizes: - LOGGER.warning( - f"Skipping the invalid file size {file_size}, " - + f"valid file sizes are {','.join(valid_file_sizes)}." - ) - file_sizes.remove(file_size) - if len(file_sizes) == 0: - file_sizes = ["original"] - photos_filters["file_sizes"] = file_sizes - - config_path[2] = "extensions" - if ( - not traverse_config_path(config=config, config_path=config_path) - or not get_config_value(config=config, config_path=config_path) - or len(get_config_value(config=config, config_path=config_path)) == 0 - ): - LOGGER.warning( - f"{config_path_to_string(config_path=config_path)} not found. Downloading all extensions ..." - ) - else: - photos_filters["extensions"] = get_config_value( - config=config, config_path=config_path - ) + # Parse albums + config_path[2] = "albums" + if ( + not traverse_config_path(config=config, config_path=config_path) + or not get_config_value(config=config, config_path=config_path) + or len(get_config_value(config=config, config_path=config_path)) == 0 + ): + LOGGER.warning( + f"{config_path_to_string(config_path=config_path)} not found. Downloading all albums ..." + ) + else: + photos_filters["albums"] = get_config_value( + config=config, config_path=config_path + ) + + # Parse file sizes + config_path[2] = "file_sizes" + if not traverse_config_path(config=config, config_path=config_path): + LOGGER.warning( + f"{config_path_to_string(config_path=config_path)} not found. Downloading original size photos ..." + ) + else: + file_sizes = get_config_value(config=config, config_path=config_path) + for file_size in file_sizes: + if file_size not in valid_file_sizes: + LOGGER.warning( + f"Skipping the invalid file size {file_size}, " + + f"valid file sizes are {','.join(valid_file_sizes)}." + ) + file_sizes.remove(file_size) + if len(file_sizes) == 0: + file_sizes = ["original"] + photos_filters["file_sizes"] = file_sizes + + # Parse extensions + config_path[2] = "extensions" + if ( + not traverse_config_path(config=config, config_path=config_path) + or not get_config_value(config=config, config_path=config_path) + or len(get_config_value(config=config, config_path=config_path)) == 0 + ): + LOGGER.warning( + f"{config_path_to_string(config_path=config_path)} not found. Downloading all extensions ..." + ) + else: + photos_filters["extensions"] = get_config_value( + config=config, config_path=config_path + ) return photos_filters diff --git a/src/sync_photos.py b/src/sync_photos.py index 5dd720433..56a0e3f65 100644 --- a/src/sync_photos.py +++ b/src/sync_photos.py @@ -162,38 +162,42 @@ def sync_photos(config, photos): filters = config_parser.get_photos_filters(config=config) files = set() download_all = config_parser.get_photos_all_albums(config=config) + libraries = ( + filters["libraries"] if filters["libraries"] is not None else photos.libraries + ) folder_format = config_parser.get_photos_folder_format(config=config) - if download_all: - for album in photos.albums.keys(): - if filters["albums"] and album in iter(filters["albums"]): - continue - sync_album( - album=photos.albums[album], - destination_path=os.path.join(destination_path, album), - file_sizes=filters["file_sizes"], - extensions=filters["extensions"], - files=files, - folder_format=folder_format, - ) - elif filters["albums"]: - for album in iter(filters["albums"]): + for library in libraries: + if download_all and library == "PrimarySync": + for album in photos.libraries[library].albums.keys(): + if filters["albums"] and album in iter(filters["albums"]): + continue + sync_album( + album=photos.libraries[library].albums[album], + destination_path=os.path.join(destination_path, album), + file_sizes=filters["file_sizes"], + extensions=filters["extensions"], + files=files, + folder_format=folder_format, + ) + elif filters["albums"] and library == "PrimarySync": + for album in iter(filters["albums"]): + sync_album( + album=photos.libraries[library].albums[album], + destination_path=os.path.join(destination_path, album), + file_sizes=filters["file_sizes"], + extensions=filters["extensions"], + files=files, + folder_format=folder_format, + ) + else: sync_album( - album=photos.albums[album], - destination_path=os.path.join(destination_path, album), + album=photos.libraries[library].all, + destination_path=os.path.join(destination_path, "all"), file_sizes=filters["file_sizes"], extensions=filters["extensions"], files=files, folder_format=folder_format, ) - else: - sync_album( - album=photos.all, - destination_path=os.path.join(destination_path, "all"), - file_sizes=filters["file_sizes"], - extensions=filters["extensions"], - files=files, - folder_format=folder_format, - ) if config_parser.get_photos_remove_obsolete(config=config): remove_obsolete(destination_path, files) diff --git a/tests/data/__init__.py b/tests/data/__init__.py index bb3947228..6b8ef705d 100644 --- a/tests/data/__init__.py +++ b/tests/data/__init__.py @@ -54,6 +54,35 @@ # Data AUTH_OK = {"authType": "hsa2"} +ZONES_LIST_WORKING = { + "zones": [ + { + "zoneID": { + "zoneName": "PrimarySync", + "ownerRecordName": "_fvhhqlzef1uvsgxnrw119mylkpjut1a0", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + "syncToken": "HwoECJGaGRgAIhYI/ZL516KyxaXfARDm2sbu7KeQiZABKAA=", + "atomic": True, + "isEligibleForZoneShare": True, + "isEligibleForHierarchicalShare": True, + "ttl": 0, + "disableZoneDuringTtl": True, + }, + { + "zoneID": { + "zoneName": "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + "ownerRecordName": "_fvhhqlzef1uvsgxnrw119mylkpjut1a0", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + "syncToken": "HwoECLiXBRgAIhUIs5Xxntqrrr9UEOLj2/q+geCjpAEoAA==", + "atomic": True, + "isEligibleForZoneShare": True, + "isEligibleForHierarchicalShare": False, + }, + ] +} + LOGIN_WORKING = { "dsInfo": { "lastName": LAST_NAME, @@ -3661,7 +3690,7 @@ "deleted": False, "zoneID": { "zoneName": "PrimarySync", - "ownerRecordName": "_0b5c3c201b3a7f1daac8ff7e7fbc0c35", + "ownerRecordName": "_fvhhqlzef1uvsgxnrw119mylkpjut1a0", "zoneType": "REGULAR_CUSTOM_ZONE", }, } @@ -3855,6 +3884,8 @@ def request(self, method, url, **kwargs): # Photos if "com.apple.photos.cloud" in url: + if url.endswith("zones/list"): + return ResponseMock(ZONES_LIST_WORKING) if "query?remapEnums=True&getCurrentSyncToken=True" in url: if data.get("query").get("recordType") == "CheckIndexingState": return ResponseMock( @@ -3962,12 +3993,37 @@ def request(self, method, url, **kwargs): "query?remapEnums=True&getCurrentSyncToken=True" ][7]["response"] ) + if ( + "zoneID" in data + and data.get("zoneID").get("zoneName") + == "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107" + ): + return ResponseMock( + photos_data.DATA[ + "query?remapEnums=True&getCurrentSyncToken=True" + ][8]["response"] + ) return ResponseMock( photos_data.DATA[ "query?remapEnums=True&getCurrentSyncToken=True" ][1]["response"] ) + if ( + data.get("query").get("recordType") + == "CPLAssetAndMasterByAddedDate" + ): + if data.get("query").get("filterBy")[0]["fieldValue"]["value"] == 0: + return ResponseMock( + photos_data.DATA[ + "query?remapEnums=True&getCurrentSyncToken=True" + ][9]["response"] + ) + return ResponseMock( + photos_data.DATA[ + "query?remapEnums=True&getCurrentSyncToken=True" + ][8]["response"] + ) if ( data.get("query").get("recordType") == "CPLContainerRelationLiveByAssetDate" @@ -4013,6 +4069,9 @@ def request(self, method, url, **kwargs): # IMG_3148.JPG another device or "https://cvws.icloud-content.com/B/ATTRy6p-Q3U1HqcF6BUKrrOMnjvoATqG89bMsXhtmMRMw009uhyJc_Kh" in url + # IMG_5513.HEIC Shared Library + or "https://cvws.icloud-content.com/B/AQDN6auXvelQyb_btBqkNNjA97E2AZ_h3_ZBuSDV7J1SfMKpllmP-FGN" + in url ): return ResponseMock( {}, @@ -4040,6 +4099,9 @@ def request(self, method, url, **kwargs): # IMG_3148.JPG another device or "https://cvws.icloud-content.com/B/Ab_8kUAhnGzSxnl9yWvh8JKBpOvWAVLSGMHt-PAQ9_krqqfXATNX57d5" in url + # IMG_5513.HEIC Shared Library + or "https://cvws.icloud-content.com/B/AY4eS1ezj9pmMHzfVzwC2CLmBwZOAXKLBx985QzfCKCGyN0wbGs6SuTf" + in url ): return ResponseMock( {}, @@ -4069,6 +4131,9 @@ def request(self, method, url, **kwargs): # IMG_3148.JPG another device or "https://cvws.icloud-content.com/B/AQNND5zpteAXnnBP2BmDd0ropjY0AV2Zh7WygJu74eNWVuuMT4lM8qme" in url + # IMG_5513.HEIC Shared Library + or "https://cvws.icloud-content.com/B/Aa_QVPVEM9bvm5Owy3GRFNyqbKuXAbgec55EhUFp9db5znXM3Xz-nq1X" + in url ): return ResponseMock( {}, diff --git a/tests/data/photos_data.py b/tests/data/photos_data.py index 875401571..94a6215a9 100644 --- a/tests/data/photos_data.py +++ b/tests/data/photos_data.py @@ -2361,6 +2361,352 @@ "syncToken": "AQAAAAAAArKjf//////////fSxWSKv5JfZ34edrt875d", }, }, + # Shared library albums + { + "data": { + "query": {"recordType": "CPLAlbumByPositionLive"}, + "zoneID": { + "zoneName": "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + "ownerRecordName": "_fvhhqlzef1uvsgxnrw119mylkpjut1a0", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + }, + "response": { + # No albums + "records": [], + "syncToken": "HwoECLiXBRgAIhUIs5Xxntqrrr9UEOLj2/q+geCjpAEoAA==", + }, + }, + # Contents of shared library all photos + { + "data": { + "query": { + "filterBy": [ + { + "fieldName": "startRank", + "fieldValue": {"type": "INT64", "value": 0}, + "comparator": "EQUALS", + }, + { + "fieldName": "direction", + "fieldValue": {"type": "STRING", "value": "ASCENDING"}, + "comparator": "EQUALS", + }, + ], + "recordType": "CPLAssetAndMasterByAddedDate", + }, + "resultsLimit": 200, + "desiredKeys": [ + "resJPEGFullWidth", + "resJPEGFullHeight", + "resJPEGFullFileType", + "resJPEGFullFingerprint", + "resJPEGFullRes", + "resJPEGLargeWidth", + "resJPEGLargeHeight", + "resJPEGLargeFileType", + "resJPEGLargeFingerprint", + "resJPEGLargeRes", + "resJPEGMedWidth", + "resJPEGMedHeight", + "resJPEGMedFileType", + "resJPEGMedFingerprint", + "resJPEGMedRes", + "resJPEGThumbWidth", + "resJPEGThumbHeight", + "resJPEGThumbFileType", + "resJPEGThumbFingerprint", + "resJPEGThumbRes", + "resVidFullWidth", + "resVidFullHeight", + "resVidFullFileType", + "resVidFullFingerprint", + "resVidFullRes", + "resVidMedWidth", + "resVidMedHeight", + "resVidMedFileType", + "resVidMedFingerprint", + "resVidMedRes", + "resVidSmallWidth", + "resVidSmallHeight", + "resVidSmallFileType", + "resVidSmallFingerprint", + "resVidSmallRes", + "resSidecarWidth", + "resSidecarHeight", + "resSidecarFileType", + "resSidecarFingerprint", + "resSidecarRes", + "itemType", + "dataClassType", + "filenameEnc", + "originalOrientation", + "resOriginalWidth", + "resOriginalHeight", + "resOriginalFileType", + "resOriginalFingerprint", + "resOriginalRes", + "resOriginalAltWidth", + "resOriginalAltHeight", + "resOriginalAltFileType", + "resOriginalAltFingerprint", + "resOriginalAltRes", + "resOriginalVidComplWidth", + "resOriginalVidComplHeight", + "resOriginalVidComplFileType", + "resOriginalVidComplFingerprint", + "resOriginalVidComplRes", + "isDeleted", + "isExpunged", + "dateExpunged", + "remappedRef", + "recordName", + "recordType", + "recordChangeTag", + "masterRef", + "adjustmentRenderType", + "assetDate", + "addedDate", + "isFavorite", + "isHidden", + "orientation", + "duration", + "assetSubtype", + "assetSubtypeV2", + "assetHDRType", + "burstFlags", + "burstFlagsExt", + "burstId", + "captionEnc", + "locationEnc", + "locationV2Enc", + "locationLatitude", + "locationLongitude", + "adjustmentType", + "timeZoneOffset", + "vidComplDurValue", + "vidComplDurScale", + "vidComplDispValue", + "vidComplDispScale", + "vidComplVisibilityState", + "customRenderedValue", + "containerId", + "itemId", + "position", + "isKeyAsset", + ], + "zoneID": { + "zoneName": "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + "ownerRecordName": "_0b5c3c201b3a7f1daac8ff7e7fbc0c35", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + }, + "response": { + "records": [ + # IMG_5513.HEIC + { + "recordName": "AY4eS1ezj9pmMHzfVzwC2CLmBwZO", + "recordType": "CPLMaster", + "fields": { + "itemType": {"value": "public.heic", "type": "STRING"}, + "resJPEGThumbFingerprint": { + "value": "DmK0xzSiAUSFrAsYYAvby7QHrMDe", + "type": "STRING", + }, + "filenameEnc": { + "value": "SU1HXzU1MTMuSEVJQw==", + "type": "ENCRYPTED_BYTES", + }, + "resJPEGMedRes": { + "value": { + "fileChecksum": "EeGlt2PppPTgd0Q7mp8GenIugSh7", + "size": 1074354, + "wrappingKey": "amaCdL9Z+QxfzgD4+aYATg==", + "referenceChecksum": "AQYmx+DRYXnMs0tkDZ3rorp4IB99", + # pylint: disable=C0321 + "downloadURL": "https://cvws.icloud-content.com/B/AQDN6auXvelQyb_btBqkNNjA97E2AZ_h3_ZBuSDV7J1SfMKpllmP-FGN/${f}?o=AkpvVanzvRePQKJ40Dm4YSvdVd9PwJFyrTyTz2oz9JPo&v=1&x=3&a=CAogVj_YvgW5M2-UxrKh2Fvp9mXaucTfmoEtSZ68oOF-ufUSbRDcod2wuDEY3P64srgxIgEAUgTA97E2WgSP-FGNaiZJrOc38Lxkny5PD-mG8_GkvEdMW-iCBLF63Zqz7ylzJVygR3BCr3ImsXnj-RgqNBe6eE3QOGEIQYnSIw1cO9KLmioI3AD_m-fQ-RL4Pfo&e=1698765356&fl=&r=1c7a694a-6450-4be0-85be-b8da170ce390-1&k=S2x_JT65p4HaFzealKoCTg&ckc=com.apple.photos.cloud&ckz=SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107&y=1&p=104&s=aw1qXk9b-vv16ld1xJ9OJ2lHSHk", # noqa: E501 + }, + "type": "ASSETID", + }, + "originalOrientation": {"value": 6, "type": "INT64"}, + "resJPEGMedHeight": {"value": 2048, "type": "INT64"}, + "resOriginalRes": { + "value": { + "fileChecksum": "YN1v8eGiHYYZ/aKUkMuGtSf0P1BN", + "size": 2194253, + "wrappingKey": "Y40xDPUr6DmxfeoSqxaQ7A==", + "referenceChecksum": "AXKVYPcDa+9Mjvnap0ZS+p2Z24V3", + # pylint: disable=C0321 + "downloadURL": "https://cvws.icloud-content.com/B/AY4eS1ezj9pmMHzfVzwC2CLmBwZOAXKLBx985QzfCKCGyN0wbGs6SuTf/${f}?o=AsBiWhNj_AjH-3vh0riCjYfhRtBfd-mUS6PQhxj4zYTH&v=1&x=3&a=CAogEeo3WZxkrjW9vg53X0JbMLiadhXSjhuENTh402ga9sISbRDdod2wuDEY3f64srgxIgEAUgTmBwZNWgQ6SuTfaiZ-CDCZGCdwB5-2MeuQ6hLpY6mmSYhtwgliGihERuDLhb89zqlO4HImWLHBIgl_7lBgvs7t6Ur5Vu_UAGoIQvIv1UEPwakPbNcGSCVnR5E&e=1698765356&fl=&r=1c7a694a-6450-4be0-85be-b8da170ce390-1&k=TF9BsVkUpEEdK0QpxQupmA&ckc=com.apple.photos.cloud&ckz=SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107&y=1&p=104&s=3nnkdl_tJa2baG_ccWB6PwXdlUM", # noqa: E501 + }, + "type": "ASSETID", + }, + "resJPEGMedFileType": { + "value": "public.jpeg", + "type": "STRING", + }, + "resJPEGThumbHeight": {"value": 480, "type": "INT64"}, + "resJPEGThumbWidth": {"value": 360, "type": "INT64"}, + "resOriginalWidth": {"value": 4032, "type": "INT64"}, + "resJPEGThumbFileType": { + "value": "public.jpeg", + "type": "STRING", + }, + "dataClassType": {"value": 1, "type": "INT64"}, + "resOriginalFingerprint": { + "value": "YN1v8eGiHYYZ/aKUkMuGtSf0P1BN", + "type": "STRING", + }, + "resJPEGMedWidth": {"value": 1536, "type": "INT64"}, + "resJPEGThumbRes": { + "value": { + "fileChecksum": "DmK0xzSiAUSFrAsYYAvby7QHrMDe", + "size": 340532, + "wrappingKey": "r7EeA3tyPsWdcECp6X9dHA==", + "referenceChecksum": "AR5TiM9Qko4rHwmoDH1BgNRVZpF4", + # pylint: disable=C0321 + "downloadURL": "https://cvws.icloud-content.com/B/Aa_QVPVEM9bvm5Owy3GRFNyqbKuXAbgec55EhUFp9db5znXM3Xz-nq1X/${f}?o=AucoksyzdsJiFVmSYa_ajMHD_2grSFU6qbVTFdggYHgq&v=1&x=3&a=CAogsS3em6hEfTnsZDnYIlTaWjcN28xkDpe_JZFfm-ELLooSbRDdod2wuDEY3f64srgxIgEAUgSqbKuXWgT-nq1XaiZcxyPbeVdgxw86mBeD2193kkQ33hkJJh_JN1ApKm98JOLBPtcV4nImJH-ycgL-oJavf11qz9IfC5_g6vlq5shkb4Ohcl6xwdHgbBIcFYE&e=1698765356&fl=&r=1c7a694a-6450-4be0-85be-b8da170ce390-1&k=n8u2-NoAEqPijfVUBMbN6Q&ckc=com.apple.photos.cloud&ckz=SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107&y=1&p=104&s=m_19S8GYm58_yhhskaWeQNM6Nug", # noqa: E501 + }, + "type": "ASSETID", + }, + "resOriginalFileType": { + "value": "public.heic", + "type": "STRING", + }, + "resOriginalHeight": {"value": 3024, "type": "INT64"}, + "resJPEGMedFingerprint": { + "value": "EeGlt2PppPTgd0Q7mp8GenIugSh7", + "type": "STRING", + }, + }, + "pluginFields": {}, + "recordChangeTag": "1u4i", + "created": { + "timestamp": 1698625916645, + "userRecordName": "_1", + "deviceID": "89219292E3E8E16382FB69349FCA9ECA559613CD2FF3C3C8C8F7B420F1C347EG", + }, + "modified": { + "timestamp": 1698625916645, + "userRecordName": "_1", + "deviceID": "89219292E3E8E16382FB69349FCA9ECA559613CD2FF3C3C8C8F7B420F1C347EG", + }, + "deleted": False, + "zoneID": { + "zoneName": "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + "ownerRecordName": "_0b5c3c201b3a7f1daac8ff7e7fbc0c35", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + }, + # Reference to IMG_5513.HEIC + { + "recordName": "92CBA085-7458-4B71-83E7-5696AA6C0437", + "recordType": "CPLAsset", + "fields": { + "addedDate": {"value": 1698601166279, "type": "TIMESTAMP"}, + "resJPEGThumbFingerprint": { + "value": "AfDQhuBu4hXYZG1MKMZww5hr0XFt", + "type": "STRING", + }, + "resJPEGFullFingerprint": { + "value": "AVUZeNmSEsGH47OEyy0AojnozAK8", + "type": "STRING", + }, + "adjustmentType": { + "value": "com.apple.photo", + "type": "STRING", + }, + "assetSubtypeV2": {"value": 0, "type": "INT64"}, + "resJPEGFullHeight": {"value": 1060, "type": "INT64"}, + "assetHDRType": {"value": 0, "type": "INT64"}, + "timeZoneOffset": {"value": -25200, "type": "INT64"}, + "masterRef": { + "value": { + "recordName": "AY4eS1ezj9pmMHzfVzwC2CLmBwZO", + "action": "DELETE_SELF", + "zoneID": { + "zoneName": "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + "ownerRecordName": "_0b5c3c201b3a7f1daac8ff7e7fbc0c35", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + }, + "type": "REFERENCE", + }, + "locationEnc": { + "value": "YnBsaXN0MDDYAQIDBAUGBwgJCgsMCg0OD1Zjb3Vyc2VVc3BlZWRTYWx0U2xvbld2ZXJ0QWNjU2", + "type": "ENCRYPTED_BYTES", + }, + "adjustmentRenderType": {"value": 2816, "type": "INT64"}, + "vidComplDispScale": {"value": 0, "type": "INT64"}, + "duration": {"value": 0, "type": "INT64"}, + "burstFlags": {"value": 0, "type": "INT64"}, + "assetSubtype": {"value": 0, "type": "INT64"}, + "resJPEGThumbWidth": {"value": 360, "type": "INT64"}, + "vidComplVisibilityState": {"value": 0, "type": "INT64"}, + "resJPEGFullWidth": {"value": 795, "type": "INT64"}, + "resJPEGFullRes": { + "value": { + "fileChecksum": "AVUZeNmSEsGH47OEyy0AojnozAK8", + "size": 340860, + "wrappingKey": "KaI99WFTkCwtFMUF+tYrSQ==", + "referenceChecksum": "AYsP/+uTEhISEYYHCK+nasKEFIie", + # pylint: disable=C0321 + "downloadURL": "https://cvws.icloud-content.com/B/AVUZeNmSEsGH47OEyy0AojnozAK8AYsP_-uTEhISEYYHCK-nasKEFIie/${f}?o=AkNOIvUnrfPDQ5THreKWKcvRplOk2ggvZF6KsaY7fd-A&v=1&x=3&a=CAogmm1yJGk7Y1lG58D_XQVryIZ6o11DEv3NIaN6iIPSKtMSbRCBpevFuDEYgYLHx7gxIgEAUgTozAK8WgSEFIieaiaHabDKDv5faXcmtytIOOSUbK4AWjU1YWdR46bN82z7JbfcangVG3ImUXOt030VnBVdnXbi-kYl85xzaurON3eMgxiRpnHZOHf0eem7cHY&e=1698809626&fl=&r=b39216b7-2243-48c2-92cb-b598e98e0d60-1&k=KaI99WFTkCwtFMUF-tYrSQ&ckc=com.apple.photos.cloud&ckz=SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D106&y=1&p=104&s=7ATOXQ_ALygO3TEGReKS2XM-L_Q", # noqa: E501 + }, + "type": "ASSETID", + }, + "assetDate": {"value": 1698601165886, "type": "TIMESTAMP"}, + "resJPEGFullFileType": { + "value": "public.heic", + "type": "STRING", + }, + "orientation": {"value": 1, "type": "INT64"}, + "isHidden": {"value": 0, "type": "INT64"}, + "resJPEGThumbHeight": {"value": 480, "type": "INT64"}, + "vidComplDurScale": {"value": 0, "type": "INT64"}, + "resJPEGThumbFileType": { + "value": "public.jpeg", + "type": "STRING", + }, + "vidComplDurValue": {"value": 0, "type": "INT64"}, + "resJPEGThumbRes": { + "value": { + "fileChecksum": "AfDQhuBu4hXYZG1MKMZww5hr0XFt", + "size": 130373, + "wrappingKey": "OhxadeoR+QL9gyG5vjchQQ==", + "referenceChecksum": "AVIWcEe38h3rZ6jdx0xradApmsm5", + # pylint: disable=C0321 + "downloadURL": "https://cvws.icloud-content.com/B/AfDQhuBu4hXYZG1MKMZww5hr0XFtAVIWcEe38h3rZ6jdx0xradApmsm5/${f}?o=Av--_tK-30ay30YC4r-3ioGEUXi0lv_jiR4L9IqMnIN1&v=1&x=3&a=CAogyaYaP5z_6cAm4ENgJsdpmA7mEWWm0bNV2NE_uHkHO5QSbRCCpevFuDEYgoLHx7gxIgEAUgRr0XFtWgQpmsm5aibYv1Ug7RFU3CVYUoQwEQushhNxX3GrnGlEPU0JXThR4gnzI-UuA3ImA-Q5OOhz9lBTE-KMkuTizKv8KxrchdoxZyDpoPXumnzP-Lz8Z9Q&e=1698809626&fl=&r=b39216b7-2243-48c2-92cb-b598e98e0d60-1&k=OhxadeoR-QL9gyG5vjchQQ&ckc=com.apple.photos.cloud&ckz=SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D106&y=1&p=104&s=XERpve2UemTKkPC2qUKPjEAq_fI", # noqa: E501 + }, + "type": "ASSETID", + }, + "customRenderedValue": {"value": 10, "type": "INT64"}, + "isFavorite": {"value": 1, "type": "INT64"}, + "vidComplDispValue": {"value": 0, "type": "INT64"}, + }, + "pluginFields": {}, + "recordChangeTag": "1u4j", + "created": { + "timestamp": 1698625916670, + "userRecordName": "_1", + "deviceID": "89219292E3E8E16382FB69349FCA9ECA559613CD2FF3C3C8C8F7B420F1C347EF", + }, + "modified": { + "timestamp": 1698625916670, + "userRecordName": "_1", + "deviceID": "89219292E3E8E16382FB69349FCA9ECA559613CD2FF3C3C8C8F7B420F1C347EF", + }, + "deleted": False, + "zoneID": { + "zoneName": "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + "ownerRecordName": "_0b5c3c201b3a7f1daac8ff7e7fbc0c35", + "zoneType": "REGULAR_CUSTOM_ZONE", + }, + }, + ], + "syncToken": "HwoECLiXBRgAIhUIs5Xxntqrrr9UEOLj2/q+geCjpAEoAA==", + }, + }, ], "query/batch?remapEnums=True&getCurrentSyncToken=True": [ { diff --git a/tests/data/test_config.yaml b/tests/data/test_config.yaml index 89112e28d..3187d45dc 100644 --- a/tests/data/test_config.yaml +++ b/tests/data/test_config.yaml @@ -34,6 +34,11 @@ drive: - "*.psd" - .git/ filters: + # List of libraries to download. If omitted (default), photos from all libraries (own and shared) are downloaded. If included, photos only + # from the listed libraries are downloaded. + # libraries: + # - PrimarySync # Name of the own library + # File filters to be included in syncing iCloud drive content folders: - dir1/dir2/dir3 @@ -56,6 +61,9 @@ photos: all_albums: false # Optional, default false. If true preserve album structure. If same photo is in multpile albums creates duplicates on filesystem # folder_format: "%Y/%m" # optional, if set put photos in subfolders according to format. Format cheatsheet - https://strftime.org filters: + # libraries: # Optional, specify list of libraries to download photos from + # - PrimarySync # Library of the user + # - SharedSync-abcd # Library of another user # if all_albums is false - albums list is used as filter-in, if all_albums is true - albums list is used as filter-out # if albums list is empty and all_albums is false download all photos to "all" folder. if empty and all_albums is true download all folders albums: diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index b0e5bf83d..1b1d8a7b7 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -444,3 +444,22 @@ def test_get_photos_folder_format_valid(self): config = read_config(config_path=tests.CONFIG_PATH) config["photos"]["folder_format"] = "%Y/%m" self.assertEqual(config_parser.get_photos_folder_format(config=config), "%Y/%m") + + def test_get_photos_filters_libraries_empty(self): + """Photos > library is missing in config.""" + config = read_config(config_path=tests.CONFIG_PATH) + self.assertEqual( + config_parser.get_photos_filters(config=config)["libraries"], None + ) + + def test_get_photos_filters_libraries_specified(self): + """Photos > library is specified as shared.""" + config = read_config(config_path=tests.CONFIG_PATH) + config["photos"]["filters"]["libraries"] = [ + "PrimarySync", + "SharedSync-9DD9B767-9F30-4D6F-B658-F17DBA16D107", + ] + self.assertListEqual( + config_parser.get_photos_filters(config=config)["libraries"], + config["photos"]["filters"]["libraries"], + ) diff --git a/tests/test_sync_photos.py b/tests/test_sync_photos.py index 329b20339..b55804e85 100644 --- a/tests/test_sync_photos.py +++ b/tests/test_sync_photos.py @@ -1,6 +1,7 @@ """Tests for sync_photos.py file.""" __author__ = "Mandar Patil (mandarons@pm.me)" +import glob import os import shutil import unittest @@ -47,6 +48,7 @@ def test_sync_photos_original( mock_service = self.service config = self.config.copy() config["photos"]["destination"] = self.destination_path + config["photos"]["filters"]["libraries"] = ["PrimarySync"] mock_read_config.return_value = config # Sync original photos self.assertIsNone( @@ -639,3 +641,29 @@ def test_photo_wanted_extensions_png( self.assertTrue(os.path.isdir(album_1_path)) self.assertTrue(len(os.listdir(album_0_path)) == 1) self.assertTrue(len(os.listdir(album_1_path)) == 0) + + @patch(target="keyring.get_password", return_value=data.VALID_PASSWORD) + @patch( + target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER + ) + @patch("icloudpy.ICloudPyService") + @patch("src.read_config") + def test_photo_download_with_shared_libraries( + self, mock_read_config, mock_service, mock_get_username, mock_get_password + ): + """Test for downloading photos from shared libraries.""" + mock_service = self.service + config = self.config.copy() + config["photos"]["destination"] = self.destination_path + del config["photos"]["filters"]["albums"] + mock_read_config.return_value = config + # Sync original photos + self.assertIsNone( + sync_photos.sync_photos(config=config, photos=mock_service.photos) + ) + all_path = os.path.join(self.destination_path, "all") + self.assertTrue(os.path.isdir(all_path)) + # Check for PrimarySync photo + self.assertTrue(len(glob.glob(os.path.join(all_path, "IMG_3148*.JPG"))) > 0) + # Check for shared photo + self.assertTrue(len(glob.glob(os.path.join(all_path, "IMG_5513*.HEIC"))) > 0)