diff --git a/README.md b/README.md index 5bd45ba9..69a12b95 100644 --- a/README.md +++ b/README.md @@ -963,7 +963,7 @@ Returns True if photo is a panorama, otherwise False. Returns a JSON representation of all photo info #### `export()` -`export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False)` +`export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False, use_albums_as_keywords=False, use_persons_as_keywords=False)` Export photo from the Photos library to another destination on disk. - dest: must be valid destination path as str (or exception raised). @@ -978,6 +978,8 @@ Export photo from the Photos library to another destination on disk. - timeout: (int, default=120) timeout in seconds used with use_photos_export - exiftool: (boolean, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path - no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes +- use_albums_as_keywords: (boolean, default = False); if True, will use album names as keywords when exporting metadata with exiftool or sidecar +- use_persons_as_keywords: (boolean, default = False); if True, will use person names as keywords when exporting metadata with exiftool or sidecar Returns: list of paths to exported files. More than one file could be exported, for example if live_photo=True, both the original imaage and the associated .mov file will be exported diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 105a274e..e56dae5f 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -895,13 +895,13 @@ def query( ) @click.option( "--person-keyword", - is_flag = True, - help = "Use person in image as keyword/tag when exporting metadata." + is_flag=True, + help="Use person in image as keyword/tag when exporting metadata.", ) @click.option( "--album-keyword", - is_flag = True, - help = "Use album name as keyword/tag when exporting metadata." + is_flag=True, + help="Use album name as keyword/tag when exporting metadata.", ) @click.option( "--current-name", @@ -1157,16 +1157,6 @@ def export( ) if photos: - # set the persons_as_keywords and albums_as_keywords on the database object - # to control metadata export - photosdb = photos[0]._db - if person_keyword: - # add persons as keywords - photosdb.use_persons_as_keywords = True - if album_keyword: - # add albums as keywords - photosdb.use_albums_as_keywords = True - if export_bursts: # add the burst_photos to the export set photos_burst = [p for p in photos if p.burst] @@ -1196,6 +1186,8 @@ def export( directory, no_extended_attributes, export_raw, + album_keyword, + person_keyword, ) else: for p in photos: @@ -1214,6 +1206,8 @@ def export( directory, no_extended_attributes, export_raw, + album_keyword, + person_keyword, ) if export_paths: click.echo(f"Exported {p.filename} to {export_paths}") @@ -1600,6 +1594,8 @@ def export_photo( directory, no_extended_attributes, export_raw, + album_keyword, + person_keyword, ): """ Helper function for export that does the actual export photo: PhotoInfo object @@ -1616,6 +1612,8 @@ def export_photo( directory: template used to determine output directory no_extended_attributes: boolean; if True, exports photo without preserving extended attributes export_raw: boolean; if True exports RAW image associate with the photo + album_keyword: boolean; if True, exports album names as keywords in metadata + person_keyword: boolean; if True, exports person names as keywords in metadata returns list of path(s) of exported photo or None if photo was missing """ @@ -1699,6 +1697,8 @@ def export_photo( use_photos_export=use_photos_export, exiftool=exiftool, no_xattr=no_extended_attributes, + use_albums_as_keywords=album_keyword, + use_persons_as_keywords=person_keyword, )[0] photo_paths.append(photo_path) @@ -1734,6 +1734,8 @@ def export_photo( use_photos_export=use_photos_export, exiftool=exiftool, no_xattr=no_extended_attributes, + use_albums_as_keywords=album_keyword, + use_persons_as_keywords=person_keyword, ) return photo_paths diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 8f257e08..a64c985e 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.28.7" +__version__ = "0.28.8" diff --git a/osxphotos/photoinfo.py b/osxphotos/photoinfo.py index 307e407f..00fd6e22 100644 --- a/osxphotos/photoinfo.py +++ b/osxphotos/photoinfo.py @@ -634,6 +634,8 @@ class PhotoInfo: timeout=120, exiftool=False, no_xattr=False, + use_albums_as_keywords=False, + use_persons_as_keywords=False, ): """ export photo dest: must be valid destination path (or exception raised) @@ -659,7 +661,12 @@ class PhotoInfo: timeout: (int, default=120) timeout in seconds used with use_photos_export exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes - returns list of full paths to the exported files """ + returns list of full paths to the exported files + use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords + when exporting metadata with exiftool or sidecar + use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords + when exporting metadata with exiftool or sidecar + """ # list of all files exported during this call to export exported_files = [] @@ -863,7 +870,10 @@ class PhotoInfo: if sidecar_json: logging.debug("writing exiftool_json_sidecar") sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.json") - sidecar_str = self._exiftool_json_sidecar() + sidecar_str = self._exiftool_json_sidecar( + use_albums_as_keywords=use_albums_as_keywords, + use_persons_as_keywords=use_persons_as_keywords, + ) try: self._write_sidecar(sidecar_filename, sidecar_str) except Exception as e: @@ -873,7 +883,10 @@ class PhotoInfo: if sidecar_xmp: logging.debug("writing xmp_sidecar") sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.xmp") - sidecar_str = self._xmp_sidecar() + sidecar_str = self._xmp_sidecar( + use_albums_as_keywords=use_albums_as_keywords, + use_persons_as_keywords=use_persons_as_keywords, + ) try: self._write_sidecar(sidecar_filename, sidecar_str) except Exception as e: @@ -883,17 +896,28 @@ class PhotoInfo: # if exiftool, write the metadata if exiftool and exported_files: for exported_file in exported_files: - self._write_exif_data(exported_file) + self._write_exif_data( + exported_file, + use_albums_as_keywords=use_albums_as_keywords, + use_persons_as_keywords=use_persons_as_keywords, + ) return exported_files - def _write_exif_data(self, filepath): + def _write_exif_data( + self, filepath, use_albums_as_keywords=False, use_persons_as_keywords=False + ): """ write exif data to image file at filepath filepath: full path to the image file """ if not os.path.exists(filepath): raise FileNotFoundError(f"Could not find file {filepath}") exiftool = ExifTool(filepath) - exif_info = json.loads(self._exiftool_json_sidecar())[0] + exif_info = json.loads( + self._exiftool_json_sidecar( + use_albums_as_keywords=use_albums_as_keywords, + use_persons_as_keywords=use_persons_as_keywords, + ) + )[0] for exiftag, val in exif_info.items(): if type(val) == list: # more than one, set first value the add additional values @@ -904,7 +928,9 @@ class PhotoInfo: else: exiftool.setvalue(exiftag, val) - def _exiftool_json_sidecar(self): + def _exiftool_json_sidecar( + self, use_albums_as_keywords=False, use_persons_as_keywords=False + ): """ return json string of EXIF details in exiftool sidecar format Does not include all the EXIF fields as those are likely already in the image Exports the following: @@ -942,10 +968,10 @@ class PhotoInfo: # filter out _UNKNOWN_PERSON person_list = [p for p in self.persons if p != _UNKNOWN_PERSON] - if self._db.use_persons_as_keywords and person_list: + if use_persons_as_keywords and person_list: keyword_list.extend(person_list) - if self._db.use_albums_as_keywords and self.albums: + if use_albums_as_keywords and self.albums: keyword_list.extend(self.albums) if keyword_list: @@ -990,7 +1016,7 @@ class PhotoInfo: json_str = json.dumps([exif]) return json_str - def _xmp_sidecar(self): + def _xmp_sidecar(self, use_albums_as_keywords=False, use_persons_as_keywords=False): """ returns string for XMP sidecar """ # TODO: add additional fields to XMP file? @@ -1007,10 +1033,10 @@ class PhotoInfo: # filter out _UNKNOWN_PERSON person_list = [p for p in self.persons if p != _UNKNOWN_PERSON] - if self._db.use_persons_as_keywords and person_list: + if use_persons_as_keywords and person_list: keyword_list.extend(person_list) - if self._db.use_albums_as_keywords and self.albums: + if use_albums_as_keywords and self.albums: keyword_list.extend(self.albums) subject_list = [] diff --git a/tests/test_export_catalina_10_15_1.py b/tests/test_export_catalina_10_15_1.py index 590f9ef7..0847a186 100644 --- a/tests/test_export_catalina_10_15_1.py +++ b/tests/test_export_catalina_10_15_1.py @@ -487,7 +487,6 @@ def test_exiftool_json_sidecar_use_persons_keyword(): import json photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photosdb.use_persons_as_keywords = True photos = photosdb.photos(uuid=[UUID_DICT["xmp"]]) json_expected = json.loads( @@ -506,7 +505,7 @@ def test_exiftool_json_sidecar_use_persons_keyword(): """ )[0] - json_got = photos[0]._exiftool_json_sidecar() + json_got = photos[0]._exiftool_json_sidecar(use_persons_as_keywords=True) json_got = json.loads(json_got)[0] # some gymnastics to account for different sort order in different pythons @@ -529,7 +528,6 @@ def test_exiftool_json_sidecar_use_albums_keyword(): import json photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photosdb.use_albums_as_keywords = True photos = photosdb.photos(uuid=[UUID_DICT["xmp"]]) json_expected = json.loads( @@ -548,7 +546,7 @@ def test_exiftool_json_sidecar_use_albums_keyword(): """ )[0] - json_got = photos[0]._exiftool_json_sidecar() + json_got = photos[0]._exiftool_json_sidecar(use_albums_as_keywords=True) json_got = json.loads(json_got)[0] # some gymnastics to account for different sort order in different pythons @@ -624,11 +622,11 @@ def test_xmp_sidecar(): for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines): assert line_expected == line_got + def test_xmp_sidecar_use_persons_keyword(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photosdb.use_persons_as_keywords = True photos = photosdb.photos(uuid=[UUID_DICT["xmp"]]) xmp_expected = """ @@ -679,17 +677,17 @@ def test_xmp_sidecar_use_persons_keyword(): xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")] - xmp_got = photos[0]._xmp_sidecar() + xmp_got = photos[0]._xmp_sidecar(use_persons_as_keywords=True) xmp_got_lines = [line.strip() for line in xmp_got.split("\n")] for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines): assert line_expected == line_got + def test_xmp_sidecar_use_albums_keyword(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photosdb.use_albums_as_keywords = True photos = photosdb.photos(uuid=[UUID_DICT["xmp"]]) xmp_expected = """ @@ -740,8 +738,8 @@ def test_xmp_sidecar_use_albums_keyword(): xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")] - xmp_got = photos[0]._xmp_sidecar() + xmp_got = photos[0]._xmp_sidecar(use_albums_as_keywords=True) xmp_got_lines = [line.strip() for line in xmp_got.split("\n")] for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines): - assert line_expected == line_got \ No newline at end of file + assert line_expected == line_got