diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 1d40f460..e742d2b2 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,5 +1,5 @@ """ version info """ -__version__ = "0.38.7" +__version__ = "0.38.8" diff --git a/osxphotos/photoinfo/_photoinfo_export.py b/osxphotos/photoinfo/_photoinfo_export.py index ae4eba45..dec00ecf 100644 --- a/osxphotos/photoinfo/_photoinfo_export.py +++ b/osxphotos/photoinfo/_photoinfo_export.py @@ -102,7 +102,7 @@ class ExportResults: ) files += [x[0] for x in self.exiftool_warning] files += [x[0] for x in self.exiftool_error] - + files = list(set(files)) return files @@ -1341,13 +1341,13 @@ def _exiftool_dict( person_list = [] if self.persons: # filter out _UNKNOWN_PERSON - person_list = sorted([p for p in self.persons if p != _UNKNOWN_PERSON]) + person_list = [p for p in self.persons if p != _UNKNOWN_PERSON] if use_persons_as_keywords and person_list: - keyword_list.extend(sorted(person_list)) + keyword_list.extend(person_list) if use_albums_as_keywords and self.albums: - keyword_list.extend(sorted(self.albums)) + keyword_list.extend(self.albums) if keyword_template: rendered_keywords = [] @@ -1382,16 +1382,19 @@ def _exiftool_dict( keyword_list.extend(rendered_keywords) if keyword_list: + # remove duplicates + keyword_list = sorted(list(set(keyword_list))) exif["XMP:TagsList"] = keyword_list.copy() exif["IPTC:Keywords"] = keyword_list.copy() if person_list: + person_list = sorted(list(set(person_list))) exif["XMP:PersonInImage"] = person_list.copy() if self.keywords or person_list: # Photos puts both keywords and persons in Subject when using "Export IPTC as XMP" # only use Photos' keywords for subject (e.g. don't include template values) - exif["XMP:Subject"] = self.keywords.copy() + person_list.copy() + exif["XMP:Subject"] = sorted(list(set(self.keywords + person_list))) # if self.favorite(): # exif["Rating"] = 5 @@ -1460,13 +1463,12 @@ def _exiftool_dict( date_utc = datetime_tz_to_utc(date) creationdate = date_utc.strftime("%Y:%m:%d %H:%M:%S") exif["QuickTime:CreateDate"] = creationdate - if self.date_modified is not None and not ignore_date_modified: + if self.date_modified is None or ignore_date_modified: + exif["QuickTime:ModifyDate"] = creationdate + else: exif["QuickTime:ModifyDate"] = datetime_tz_to_utc( self.date_modified ).strftime("%Y:%m:%d %H:%M:%S") - else: - exif["QuickTime:ModifyDate"] = creationdate - return exif @@ -1604,6 +1606,15 @@ def _xmp_sidecar( # Photos puts both keywords and persons in Subject when using "Export IPTC as XMP" subject_list = list(self.keywords) + person_list + # remove duplicates + # sorted mainly to make testing the XMP file easier + if keyword_list: + keyword_list = sorted(list(set(keyword_list))) + if subject_list: + subject_list = sorted(list(set(subject_list))) + if person_list: + person_list = sorted(list(set(person_list))) + xmp_str = xmp_template.render( photo=self, description=description, diff --git a/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-shm b/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-shm index c640b0d9..cd19c001 100644 Binary files a/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-shm and b/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-shm differ diff --git a/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-wal b/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-wal index fcddc203..514d5d73 100644 Binary files a/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-wal and b/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-wal differ diff --git a/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite.lock b/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite.lock index 2c98250f..4c746646 100644 --- a/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite.lock +++ b/tests/Test-10.15.7.photoslibrary/database/Photos.sqlite.lock @@ -7,7 +7,7 @@ hostuuid 9575E48B-8D5F-5654-ABAC-4431B1167324 pid - 19275 + 55247 processname photolibraryd uid diff --git a/tests/Test-10.15.7.photoslibrary/database/search/psi.sqlite b/tests/Test-10.15.7.photoslibrary/database/search/psi.sqlite index a8d06033..4a974570 100644 Binary files a/tests/Test-10.15.7.photoslibrary/database/search/psi.sqlite and b/tests/Test-10.15.7.photoslibrary/database/search/psi.sqlite differ diff --git a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm index 5b258157..587fe75a 100644 Binary files a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm and b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm differ diff --git a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal index fc1a45c0..d07a0a36 100644 Binary files a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal and b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal differ diff --git a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm index 60b593d1..0c057888 100644 Binary files a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm and b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm differ diff --git a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist index 0423769d..9348bd5c 100644 Binary files a/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist and b/tests/Test-10.15.7.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist differ diff --git a/tests/Test-10.15.7.photoslibrary/resources/journals/Asset-change.plj b/tests/Test-10.15.7.photoslibrary/resources/journals/Asset-change.plj index 620d8b22..2906c9f0 100644 Binary files a/tests/Test-10.15.7.photoslibrary/resources/journals/Asset-change.plj and b/tests/Test-10.15.7.photoslibrary/resources/journals/Asset-change.plj differ diff --git a/tests/Test-10.15.7.photoslibrary/resources/journals/HistoryToken.plist b/tests/Test-10.15.7.photoslibrary/resources/journals/HistoryToken.plist index d28f2997..93c0060d 100644 Binary files a/tests/Test-10.15.7.photoslibrary/resources/journals/HistoryToken.plist and b/tests/Test-10.15.7.photoslibrary/resources/journals/HistoryToken.plist differ diff --git a/tests/Test-10.15.7.photoslibrary/resources/journals/Keyword-change.plj b/tests/Test-10.15.7.photoslibrary/resources/journals/Keyword-change.plj index 356190ed..d90cabbf 100644 Binary files a/tests/Test-10.15.7.photoslibrary/resources/journals/Keyword-change.plj and b/tests/Test-10.15.7.photoslibrary/resources/journals/Keyword-change.plj differ diff --git a/tests/test_catalina_10_15_7.py b/tests/test_catalina_10_15_7.py index 7f1b7364..fb4bd5a0 100644 --- a/tests/test_catalina_10_15_7.py +++ b/tests/test_catalina_10_15_7.py @@ -35,6 +35,7 @@ KEYWORDS = [ "United Kingdom", "foo/bar", "Travel", + "Maria", ] # Photos 5 includes blank person for detected face PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] @@ -60,6 +61,7 @@ KEYWORDS_DICT = { "United Kingdom": 1, "foo/bar": 1, "Travel": 2, + "Maria": 1, } PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 2, _UNKNOWN_PERSON: 1} ALBUM_DICT = { @@ -339,7 +341,7 @@ def test_attributes_2(photosdb): photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) assert len(photos) == 1 p = photos[0] - assert p.keywords == ["wedding"] + assert sorted(p.keywords) == ["Maria", "wedding"] assert p.original_filename == "wedding.jpg" assert p.filename == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg" assert p.date == datetime.datetime( diff --git a/tests/test_cli.py b/tests/test_cli.py index 7c07ed75..02489398 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -407,6 +407,10 @@ CLI_EXIFTOOL_IGNORE_DATE_MODIFIED = { CLI_EXIFTOOL_ERROR = ["E2078879-A29C-4D6F-BACB-E3BBE6C3EB91"] +CLI_EXIFTOOL_DUPLICATE_KEYWORDS = { + "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": "wedding.jpg" +} + LABELS_JSON = { "labels": { "Plant": 7, @@ -1017,7 +1021,10 @@ def test_export_exiftool(): exif = ExifTool(CLI_EXIFTOOL[uuid]["File:FileName"]).asdict() for key in CLI_EXIFTOOL[uuid]: - assert exif[key] == CLI_EXIFTOOL[uuid][key] + if type(exif[key]) == list: + assert sorted(exif[key]) == sorted(CLI_EXIFTOOL[uuid][key]) + else: + assert exif[key] == CLI_EXIFTOOL[uuid][key] @pytest.mark.skipif(exiftool is None, reason="exiftool not installed") @@ -1051,7 +1058,10 @@ def test_export_exiftool_ignore_date_modified(): CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]["File:FileName"] ).asdict() for key in CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]: - assert exif[key] == CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key] + if type(exif[key]) == list: + assert sorted(exif[key]) == sorted(CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key]) + else: + assert exif[key] == CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key] @pytest.mark.skipif(exiftool is None, reason="exiftool not installed") @@ -1093,6 +1103,38 @@ def test_export_exiftool_quicktime(): for filename in files: os.unlink(filename) + +@pytest.mark.skipif(exiftool is None, reason="exiftool not installed") +def test_export_exiftool_duplicate_keywords(): + """ ensure duplicate keywords are removed """ + import glob + import os + import os.path + from osxphotos.__main__ import export + from osxphotos.exiftool import ExifTool + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + for uuid in CLI_EXIFTOOL_DUPLICATE_KEYWORDS: + result = runner.invoke( + export, + [ + os.path.join(cwd, PHOTOS_DB_15_7), + ".", + "-V", + "--exiftool", + "--uuid", + f"{uuid}", + ], + ) + exif = ExifTool(CLI_EXIFTOOL_DUPLICATE_KEYWORDS[uuid]) + exifdict = exif.asdict() + assert sorted(exifdict["IPTC:Keywords"]) == ["Maria", "wedding"] + assert sorted(exifdict["XMP:Subject"]) == ["Maria", "wedding"] + + @pytest.mark.skipif(exiftool is None, reason="exiftool not installed") def test_export_exiftool_error(): """" test --exiftool catching error """ @@ -1106,7 +1148,7 @@ def test_export_exiftool_error(): cwd = os.getcwd() # pylint: disable=not-context-manager with runner.isolated_filesystem(): - for uuid in CLI_EXIFTOOL_ERROR: + for uuid in CLI_EXIFTOOL: result = runner.invoke( export, [ @@ -1119,7 +1161,15 @@ def test_export_exiftool_error(): ], ) assert result.exit_code == 0 - assert "exiftool error" in result.output + files = glob.glob("*") + assert sorted(files) == sorted([CLI_EXIFTOOL[uuid]["File:FileName"]]) + + exif = ExifTool(CLI_EXIFTOOL[uuid]["File:FileName"]).asdict() + for key in CLI_EXIFTOOL[uuid]: + if type(exif[key]) == list: + assert sorted(exif[key]) == sorted(CLI_EXIFTOOL[uuid][key]) + else: + assert exif[key] == CLI_EXIFTOOL[uuid][key] def test_export_edited_suffix(): diff --git a/tests/test_export_catalina_10_15_7.py b/tests/test_export_catalina_10_15_7.py index 3bdd7426..708678e4 100644 --- a/tests/test_export_catalina_10_15_7.py +++ b/tests/test_export_catalina_10_15_7.py @@ -22,6 +22,7 @@ KEYWORDS = [ "St. James's Park", "UK", "United Kingdom", + "Maria" ] # Photos 5 includes blank person for detected face PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] @@ -39,6 +40,7 @@ KEYWORDS_DICT = { "St. James's Park": 1, "UK": 1, "United Kingdom": 1, + "Maria": 1, } PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1} ALBUM_DICT = { @@ -70,8 +72,8 @@ EXIF_JSON_UUID = UUID_DICT["has_adjustments"] EXIF_JSON_EXPECTED = """ [{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", - "XMP:TagsList": ["wedding"], - "IPTC:Keywords": ["wedding"], + "XMP:TagsList": ["Maria", "wedding"], + "IPTC:Keywords": ["Maria", "wedding"], "XMP:PersonInImage": ["Maria"], "XMP:Subject": ["wedding", "Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", @@ -85,8 +87,8 @@ EXIF_JSON_EXPECTED = """ EXIF_JSON_EXPECTED_IGNORE_DATE_MODIFIED = """ [{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", - "XMP:TagsList": ["wedding"], - "IPTC:Keywords": ["wedding"], + "XMP:TagsList": ["Maria", "wedding"], + "IPTC:Keywords": ["Maria", "wedding"], "XMP:PersonInImage": ["Maria"], "XMP:Subject": ["wedding", "Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", @@ -522,8 +524,8 @@ def test_exiftool_json_sidecar_keyword_template_long(caplog): """ [{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", - "XMP:TagsList": ["wedding", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"], - "IPTC:Keywords": ["wedding", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"], + "XMP:TagsList": ["Maria", "wedding", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"], + "IPTC:Keywords": ["Maria", "wedding", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"], "XMP:PersonInImage": ["Maria"], "XMP:Subject": ["wedding", "Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", @@ -571,8 +573,8 @@ def test_exiftool_json_sidecar_keyword_template(): """ [{"EXIF:ImageDescription": "Bride Wedding day", "XMP:Description": "Bride Wedding day", - "XMP:TagsList": ["wedding", "Folder1/SubFolder2/AlbumInFolder", "I have a deleted twin"], - "IPTC:Keywords": ["wedding", "Folder1/SubFolder2/AlbumInFolder", "I have a deleted twin"], + "XMP:TagsList": ["Maria", "wedding", "Folder1/SubFolder2/AlbumInFolder", "I have a deleted twin"], + "IPTC:Keywords": ["Maria", "wedding", "Folder1/SubFolder2/AlbumInFolder", "I have a deleted twin"], "XMP:PersonInImage": ["Maria"], "XMP:Subject": ["wedding", "Maria"], "EXIF:DateTimeOriginal": "2019:04:15 14:40:24", diff --git a/tests/test_export_mojave_10_14_6.py b/tests/test_export_mojave_10_14_6.py index 37c74f78..24e93011 100644 --- a/tests/test_export_mojave_10_14_6.py +++ b/tests/test_export_mojave_10_14_6.py @@ -427,9 +427,9 @@ def test_xmp_sidecar(): + Katie Kids Suzy - Katie 2018-09-28T15:35:49.063000-04:00 @@ -438,8 +438,8 @@ def test_xmp_sidecar(): xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'> - Suzy Katie + Suzy