From bf2a55d7f645591acadf20b9de446ef72031921b Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Thu, 31 Dec 2020 12:33:15 -0800 Subject: [PATCH] Added --xattr-template, closes #242 --- README.md | 85 ++++++++++---- osxphotos/__main__.py | 138 ++++++++++++++++++++--- osxphotos/_constants.py | 16 ++- osxphotos/_version.py | 2 +- setup.py | 2 +- tests/search_info_test_data_10_15_7.json | 2 +- tests/test_cli.py | 93 +++++++++++++++ 7 files changed, 297 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 482088f0..219ad727 100644 --- a/README.md +++ b/README.md @@ -412,20 +412,30 @@ Options: could specify --description-template "{descr} exported with osxphotos on {today.date}" See Templating System below. - --finder-tag-template TEMPLATE Set Finder tags to TEMPLATE. These tags can - be searched in the Finder or Spotlight with - 'tag:tagname' format. For example, '-- - finder-tag-template "{label}"' to set Finder - tags to photo labels. You may specify - multiple TEMPLATE values by using '--finder- - tag-template' multiple times. See also '-- - finder-tag-keywords and Extended Attributes - below.'. - --finder-tag-keywords Set Finder tags to keywords; any keywords - specified via '--keyword-template', '-- - person-keyword', etc. will also be used as - Finder tags. See also '--finder-tag-template - and Extended Attributes below.'. + --finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These + tags can be searched in the Finder or + Spotlight with 'tag:tagname' format. For + example, '--finder-tag-template "{label}"' + to set Finder tags to photo labels. You may + specify multiple TEMPLATE values by using ' + --finder-tag-template' multiple times. See + also '--finder-tag-keywords and Extended + Attributes below.'. + --finder-tag-keywords Set MacOS Finder tags to keywords; any + keywords specified via '--keyword-template', + '--person-keyword', etc. will also be used + as Finder tags. See also '--finder-tag- + template and Extended Attributes below.'. + --xattr-template ATTRIBUTE TEMPLATE + Set extended attribute ATTRIBUTE to TEMPLATE + value. Valid attributes are: 'authors', + 'comment', 'copyright', 'description', + 'findercomment', 'headline', 'keywords'. For + example, to set Finder comment to the + photo's title and description: '--xattr- + template findercomment "{title}; {descr}" + See Extended Attributes below for additional + details on this option. --directory DIRECTORY Optional template for specifying name of output directory in the form '{name,DEFAULT}'. See below for additional @@ -530,21 +540,50 @@ option to re-export the entire library thus rebuilding the ** Extended Attributes ** -Some options (currently '--finder-tag-template' and '--finder-tag-keywords') -write additional metadata to extended attributes in the file. These options -will only work if the destination filesystem supports extended attributes -(most do). For example, --finder-tag-keyword writes all keywords (including -any specified by '--keyword-template' or other options) to Finder tags that -are searchable in Spotlight using the syntax: 'tag:tagname'. For example, if -you have images with keyword "Travel" then using '--finder-tag-keywords' you -could quickly find those images in the Finder by typing 'tag:Travel' in the -Spotlight search bar. Finder tags are written to the +Some options (currently '--finder-tag-template', '--finder-tag-keywords', +'-xattr-template') write additional metadata to extended attributes in the +file. These options will only work if the destination filesystem supports +extended attributes (most do). For example, --finder-tag-keyword writes all +keywords (including any specified by '--keyword-template' or other options) to +Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'. +For example, if you have images with keyword "Travel" then using '--finder- +tag-keywords' you could quickly find those images in the Finder by typing +'tag:Travel' in the Spotlight search bar. Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute. Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes will cause Dropbox to re-sync the files. +The following attributes may be used with '--xattr-template': + + +authors The author, or authors, of the contents of the file. A list + of strings. (com.apple.metadata:kMDItemAuthors) +comment A comment related to the file. This differs from the Finder + comment, kMDItemFinderComment. A string. + (com.apple.metadata:kMDItemComment) +copyright The copyright owner of the file contents. A string. + (com.apple.metadata:kMDItemCopyright) +description A description of the content of the resource. The + description may include an abstract, table of contents, + reference to a graphical representation of content or a free- + text account of the content. A string. + (com.apple.metadata:kMDItemDescription) +findercomment Finder comments for this file. A string. + (com.apple.metadata:kMDItemFinderComment) +headline A publishable entry providing a synopsis of the contents of + the file. A string. (com.apple.metadata:kMDItemHeadline) +keywords Keywords associated with this file. For example, “Birthday”, + “Important”, etc. This differs from Finder tags + (_kMDItemUserTags) which are keywords/tags shown in the + Finder and searchable in Spotlight using "tag:tag_name". A + list of strings. (com.apple.metadata:kMDItemKeywords) + +For additional information on extended attributes see: https://developer.apple +.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute +_keys + ** Templating System ** diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 2f25f355..f049ebd6 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -11,21 +11,23 @@ import time import unicodedata import click +import osxmetadata import yaml -import osxmetadata import osxphotos from ._constants import ( _EXIF_TOOL_URL, + _OSXPHOTOS_NONE_SENTINEL, _PHOTOS_4_VERSION, _UNKNOWN_PLACE, - _OSXPHOTOS_NONE_SENTINEL, CLI_COLOR_ERROR, CLI_COLOR_WARNING, DEFAULT_EDITED_SUFFIX, DEFAULT_JPEG_QUALITY, DEFAULT_ORIGINAL_SUFFIX, + EXTENDED_ATTRIBUTE_NAMES, + EXTENDED_ATTRIBUTE_NAMES_QUOTED, SIDECAR_EXIFTOOL, SIDECAR_JSON, SIDECAR_XMP, @@ -186,7 +188,7 @@ class ExportCommand(click.Command): formatter.write("\n") formatter.write_text( """ -Some options (currently '--finder-tag-template' and '--finder-tag-keywords') write +Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write additional metadata to extended attributes in the file. These options will only work if the destination filesystem supports extended attributes (most do). For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template' @@ -197,9 +199,24 @@ Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended at Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes will cause Dropbox to re-sync the files. + +The following attributes may be used with '--xattr-template': + """ ) - + formatter.write_dl( + [ + ( + attr, + f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})", + ) + for attr in EXTENDED_ATTRIBUTE_NAMES + ] + ) + formatter.write("\n") + formatter.write_text( + "For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys" + ) formatter.write("\n\n") formatter.write_text("** Templating System **") formatter.write("\n") @@ -1490,6 +1507,17 @@ def query( help="Set MacOS Finder tags to keywords; any keywords specified via '--keyword-template', '--person-keyword', etc. " "will also be used as Finder tags. See also '--finder-tag-template and Extended Attributes below.'.", ) +@click.option( + "--xattr-template", + nargs=2, + metavar="ATTRIBUTE TEMPLATE", + multiple=True, + help="Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: " + f"{', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}. " + "For example, to set Finder comment to the photo's title and description: " + "'--xattr-template findercomment \"{title}; {descr}\" " + "See Extended Attributes below for additional details on this option.", +) @click.option( "--directory", metavar="DIRECTORY", @@ -1632,6 +1660,7 @@ def export( description_template, finder_tag_template, finder_tag_keywords, + xattr_template, current_name, convert_to_jpeg, jpeg_quality, @@ -1770,6 +1799,7 @@ def export( description_template = cfg.description_template finder_tag_template = cfg.finder_tag_template finder_tag_keywords = cfg.finder_tag_keywords + xattr_template = cfg.xattr_template current_name = cfg.current_name convert_to_jpeg = cfg.convert_to_jpeg jpeg_quality = cfg.jpeg_quality @@ -1884,6 +1914,19 @@ def export( ) raise click.Abort() + if xattr_template: + for attr, _ in xattr_template: + if attr not in EXTENDED_ATTRIBUTE_NAMES: + click.echo( + click.style( + f"Invalid attribute '{attr}' for --xattr-template; " + f"valid values are {', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + raise click.Abort() + if save_config: verbose_(f"Saving options to file {save_config}") cfg.write_to_file(save_config) @@ -2160,18 +2203,21 @@ def export( ) results += export_results + # all photo files (not including sidecars) that are part of this export set + # used below for applying Finder tags, etc. + photo_files = set( + export_results.exported + + export_results.new + + export_results.updated + + export_results.exif_updated + + export_results.converted_to_jpeg + + export_results.skipped + ) + if finder_tag_keywords or finder_tag_template: - files = set( - export_results.exported - + export_results.new - + export_results.updated - + export_results.exif_updated - + export_results.converted_to_jpeg - + export_results.skipped - ) tags_written, tags_skipped = write_finder_tags( p, - files, + photo_files, keywords=finder_tag_keywords, keyword_template=keyword_template, album_keyword=album_keyword, @@ -2182,6 +2228,13 @@ def export( results.xattr_written.extend(tags_written) results.xattr_skipped.extend(tags_skipped) + if xattr_template: + xattr_written, xattr_skipped = write_extended_attributes( + p, photo_files, xattr_template + ) + results.xattr_written.extend(xattr_written) + results.xattr_skipped.extend(xattr_skipped) + if fp is not None: fp.close() @@ -3444,5 +3497,64 @@ def write_finder_tags( return (written, skipped) +def write_extended_attributes(photo, files, xattr_template): + """ Writes extended attributes to exported files + + Args: + photo: a PhotoInfo object + xattr_template: list of tuples: (attribute name, attribute template) + + Returns: + tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating) + """ + + attributes = {} + for xattr, template_str in xattr_template: + rendered, unmatched = photo.render_template( + template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/" + ) + if unmatched: + click.echo( + click.style( + f"Warning: unmatched template substitution for template: {template_str} {unmatched}", + fg=CLI_COLOR_WARNING, + ), + err=True, + ) + # filter out any template values that didn't match by looking for sentinel + rendered = [ + value for value in rendered if _OSXPHOTOS_NONE_SENTINEL not in value + ] + try: + attributes[xattr].extend(rendered) + except KeyError: + attributes[xattr] = rendered + + written = set() + skipped = set() + for f in files: + md = osxmetadata.OSXMetaData(f) + for attr, value in attributes.items(): + islist = osxmetadata.ATTRIBUTES[attr].list + if value: + value = ", ".join(value) if not islist else sorted(value) + file_value = md.get_attribute(attr) + + if file_value and islist: + file_value = sorted(file_value) + + if (not file_value and not value) or file_value == value: + # if both not set or both equal, nothing to do + # get_attribute returns None if not set and value will be [] if not set so can't directly compare + verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do") + skipped.add(f) + else: + verbose_(f"Writing extended attribute {attr} to {f}") + md.set_attribute(attr, value) + written.add(f) + + return list(written), [f for f in skipped if f not in written] + + if __name__ == "__main__": cli() # pylint: disable=no-value-for-parameter diff --git a/osxphotos/_constants.py b/osxphotos/_constants.py index f44b1a5a..3bca2c34 100644 --- a/osxphotos/_constants.py +++ b/osxphotos/_constants.py @@ -108,7 +108,7 @@ SEARCH_CATEGORY_NEIGHBORHOOD = 3 SEARCH_CATEGORY_LOCALITY_4 = 4 SEARCH_CATEGORY_SUB_LOCALITY_5 = 5 SEARCH_CATEGORY_SUB_LOCALITY_6 = 6 -SEARCH_CATEGORY_CITY = 7 +SEARCH_CATEGORY_CITY = 7 SEARCH_CATEGORY_LOCALITY_8 = 8 SEARCH_CATEGORY_NAMED_AREA = 9 SEARCH_CATEGORY_ALL_LOCALITY = [ @@ -182,4 +182,16 @@ CLI_COLOR_WARNING = "yellow" # Bit masks for --sidecar SIDECAR_JSON = 0x1 SIDECAR_EXIFTOOL = 0x2 -SIDECAR_XMP = 0x4 \ No newline at end of file +SIDECAR_XMP = 0x4 + +# supported attributes for --xattr-template +EXTENDED_ATTRIBUTE_NAMES = [ + "authors", + "comment", + "copyright", + "description", + "findercomment", + "headline", + "keywords", +] +EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES] diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 781d74ee..8c62e9ae 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,5 +1,5 @@ """ version info """ -__version__ = "0.39.1" +__version__ = "0.39.2" diff --git a/setup.py b/setup.py index 98359fee..ec801c87 100755 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ setup( "wurlitzer>=2.0.1", "photoscript>=0.1.0", "toml>=0.10.0", - "osxmetadata>=0.99.11", + "osxmetadata>=0.99.13", ], entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]}, include_package_data=True, diff --git a/tests/search_info_test_data_10_15_7.json b/tests/search_info_test_data_10_15_7.json index 633ea061..ac1a61f6 100644 --- a/tests/search_info_test_data_10_15_7.json +++ b/tests/search_info_test_data_10_15_7.json @@ -1 +1 @@ -{"UUID_SEARCH_INFO": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["Food", "Butter"], "place_names": ["Durham Bulls Athletic Park"], "streets": ["Blackwell St"], "neighborhoods": ["American Tobacco District", "Downtown Durham"], "city": "Durham", "locality_names": ["Durham"], "state": "North Carolina", "state_abbreviation": "NC", "country": "United States", "bodies_of_water": [], "month": "October", "year": "2018", "holidays": [], "activities": ["Dinner", "Travel", "Entertainment", "Dining", "Trip"], "season": "Fall", "venues": ["Luna Rotisserie and Empanadas", "Copa", "Pie Pusher's", "The Pinhook"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["Land", "Desert", "Outdoor", "Sky", "Sunset Sunrise"], "place_names": ["Royal Palms State Beach"], "streets": [], "neighborhoods": ["San Pedro"], "city": "Los Angeles", "locality_names": [], "state": "California", "state_abbreviation": "", "country": "United States", "bodies_of_water": ["Catalina Channel"], "month": "November", "year": "2017", "holidays": [], "activities": ["Beach Activity", "Activity"], "season": "Fall", "venues": [], "venue_types": [], "media_types": ["Live Photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["Furniture", "Bench", "Vegetation", "Forest", "People", "Outdoor", "Land", "Water", "Water Body"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "December", "year": "2014", "holidays": ["Christmas Day"], "activities": ["Celebration", "Holiday"], "season": "Winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["food", "butter"], "place_names": ["durham bulls athletic park"], "streets": ["blackwell st"], "neighborhoods": ["american tobacco district", "downtown durham"], "city": "durham", "locality_names": ["durham"], "state": "north carolina", "state_abbreviation": "nc", "country": "united states", "bodies_of_water": [], "month": "october", "year": "2018", "holidays": [], "activities": ["dinner", "travel", "entertainment", "dining", "trip"], "season": "fall", "venues": ["luna rotisserie and empanadas", "copa", "pie pusher's", "the pinhook"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["land", "desert", "outdoor", "sky", "sunset sunrise"], "place_names": ["royal palms state beach"], "streets": [], "neighborhoods": ["san pedro"], "city": "los angeles", "locality_names": [], "state": "california", "state_abbreviation": "", "country": "united states", "bodies_of_water": ["catalina channel"], "month": "november", "year": "2017", "holidays": [], "activities": ["beach activity", "activity"], "season": "fall", "venues": [], "venue_types": [], "media_types": ["live photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["furniture", "bench", "vegetation", "forest", "people", "outdoor", "land", "water", "water body"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "december", "year": "2014", "holidays": ["christmas day"], "activities": ["celebration", "holiday"], "season": "winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_ALL": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["Food", "Butter", "Durham Bulls Athletic Park", "Blackwell St", "American Tobacco District", "Downtown Durham", "Durham", "Dinner", "Travel", "Entertainment", "Dining", "Trip", "Luna Rotisserie and Empanadas", "Copa", "Pie Pusher's", "The Pinhook", "Durham", "North Carolina", "NC", "United States", "October", "2018", "Fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["Land", "Desert", "Outdoor", "Sky", "Sunset Sunrise", "Royal Palms State Beach", "San Pedro", "Catalina Channel", "Beach Activity", "Activity", "Live Photos", "Los Angeles", "California", "United States", "November", "2017", "Fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["Furniture", "Bench", "Vegetation", "Forest", "People", "Outdoor", "Land", "Water", "Water Body", "Christmas Day", "Celebration", "Holiday", "December", "2014", "Winter"]}, "UUID_SEARCH_INFO_ALL_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["food", "butter", "durham bulls athletic park", "blackwell st", "american tobacco district", "downtown durham", "durham", "dinner", "travel", "entertainment", "dining", "trip", "luna rotisserie and empanadas", "copa", "pie pusher's", "the pinhook", "durham", "north carolina", "nc", "united states", "october", "2018", "fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["land", "desert", "outdoor", "sky", "sunset sunrise", "royal palms state beach", "san pedro", "catalina channel", "beach activity", "activity", "live photos", "los angeles", "california", "united states", "november", "2017", "fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["furniture", "bench", "vegetation", "forest", "people", "outdoor", "land", "water", "water body", "christmas day", "celebration", "holiday", "december", "2014", "winter"]}} +{"UUID_SEARCH_INFO": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["Butter", "Food"], "place_names": ["Durham Bulls Athletic Park"], "streets": ["Blackwell St"], "neighborhoods": ["American Tobacco District", "Downtown Durham"], "city": "Durham", "locality_names": ["Durham"], "state": "North Carolina", "state_abbreviation": "NC", "country": "United States", "bodies_of_water": [], "month": "October", "year": "2018", "holidays": [], "activities": ["Dinner", "Travel", "Entertainment", "Dining", "Trip"], "season": "Fall", "venues": ["Copa", "Pie Pusher's", "Luna Rotisserie and Empanadas", "The Pinhook"], "venue_types": ["Nightlife", "Cocktail Bar", "Pizza", "Restaurant", "Bar", "Tapas & Small Plates", "Food", "Empanadas", "Arts & Entertainment", "Chicken Wings", "Latin American", "Cuban", "Music Venue"], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["Sunset Sunrise", "Desert", "Sky", "Outdoor", "Land"], "place_names": ["Royal Palms State Beach"], "streets": [], "neighborhoods": ["San Pedro"], "city": "Los Angeles", "locality_names": [], "state": "California", "state_abbreviation": "", "country": "United States", "bodies_of_water": ["Catalina Channel"], "month": "November", "year": "2017", "holidays": [], "activities": ["Beach Activity", "Activity"], "season": "Fall", "venues": [], "venue_types": [], "media_types": ["Live Photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["Forest", "Land", "Water Body", "Furniture", "Bench", "Water", "People", "Plant", "Outdoor", "Vegetation"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "December", "year": "2014", "holidays": ["Christmas Day"], "activities": ["Celebration", "Holiday"], "season": "Winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["butter", "food"], "place_names": ["durham bulls athletic park"], "streets": ["blackwell st"], "neighborhoods": ["american tobacco district", "downtown durham"], "city": "durham", "locality_names": ["durham"], "state": "north carolina", "state_abbreviation": "nc", "country": "united states", "bodies_of_water": [], "month": "october", "year": "2018", "holidays": [], "activities": ["dinner", "travel", "entertainment", "dining", "trip"], "season": "fall", "venues": ["copa", "pie pusher's", "luna rotisserie and empanadas", "the pinhook"], "venue_types": ["nightlife", "cocktail bar", "pizza", "restaurant", "bar", "tapas & small plates", "food", "empanadas", "arts & entertainment", "chicken wings", "latin american", "cuban", "music venue"], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["sunset sunrise", "desert", "sky", "outdoor", "land"], "place_names": ["royal palms state beach"], "streets": [], "neighborhoods": ["san pedro"], "city": "los angeles", "locality_names": [], "state": "california", "state_abbreviation": "", "country": "united states", "bodies_of_water": ["catalina channel"], "month": "november", "year": "2017", "holidays": [], "activities": ["beach activity", "activity"], "season": "fall", "venues": [], "venue_types": [], "media_types": ["live photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["forest", "land", "water body", "furniture", "bench", "water", "people", "plant", "outdoor", "vegetation"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "december", "year": "2014", "holidays": ["christmas day"], "activities": ["celebration", "holiday"], "season": "winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_ALL": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["Butter", "Food", "Durham Bulls Athletic Park", "Blackwell St", "American Tobacco District", "Downtown Durham", "Durham", "Dinner", "Travel", "Entertainment", "Dining", "Trip", "Copa", "Pie Pusher's", "Luna Rotisserie and Empanadas", "The Pinhook", "Nightlife", "Cocktail Bar", "Pizza", "Restaurant", "Bar", "Tapas & Small Plates", "Food", "Empanadas", "Arts & Entertainment", "Chicken Wings", "Latin American", "Cuban", "Music Venue", "Durham", "North Carolina", "NC", "United States", "October", "2018", "Fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["Sunset Sunrise", "Desert", "Sky", "Outdoor", "Land", "Royal Palms State Beach", "San Pedro", "Catalina Channel", "Beach Activity", "Activity", "Live Photos", "Los Angeles", "California", "United States", "November", "2017", "Fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["Forest", "Land", "Water Body", "Furniture", "Bench", "Water", "People", "Plant", "Outdoor", "Vegetation", "Christmas Day", "Celebration", "Holiday", "December", "2014", "Winter"]}, "UUID_SEARCH_INFO_ALL_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["butter", "food", "durham bulls athletic park", "blackwell st", "american tobacco district", "downtown durham", "durham", "dinner", "travel", "entertainment", "dining", "trip", "copa", "pie pusher's", "luna rotisserie and empanadas", "the pinhook", "nightlife", "cocktail bar", "pizza", "restaurant", "bar", "tapas & small plates", "food", "empanadas", "arts & entertainment", "chicken wings", "latin american", "cuban", "music venue", "durham", "north carolina", "nc", "united states", "october", "2018", "fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["sunset sunrise", "desert", "sky", "outdoor", "land", "royal palms state beach", "san pedro", "catalina channel", "beach activity", "activity", "live photos", "los angeles", "california", "united states", "november", "2017", "fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["forest", "land", "water body", "furniture", "bench", "water", "people", "plant", "outdoor", "vegetation", "christmas day", "celebration", "holiday", "december", "2014", "winter"]}} diff --git a/tests/test_cli.py b/tests/test_cli.py index 83964fa7..bb9453c1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5115,3 +5115,96 @@ def test_export_finder_tag_template_keywords(): persons = [persons] if type(persons) != list else persons expected = [Tag(x) for x in keywords + persons] assert sorted(md.tags) == sorted(expected) + + +def test_export_xattr_template(): + """ test --xattr template """ + import glob + import os + import os.path + + from osxmetadata import OSXMetaData + from osxphotos.__main__ import export + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + for uuid in CLI_FINDER_TAGS: + result = runner.invoke( + export, + [ + os.path.join(cwd, PHOTOS_DB_15_7), + ".", + "-V", + "--xattr-template", + "keywords", + "{person}", + "--xattr-template", + "comment", + "{title}", + "--uuid", + f"{uuid}", + ], + ) + assert result.exit_code == 0 + + md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"]) + expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"] + expected = [expected] if type(expected) != list else expected + assert sorted(md.keywords) == sorted(expected) + assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"] + + # run again with --update, should skip writing extended attributes + result = runner.invoke( + export, + [ + os.path.join(cwd, PHOTOS_DB_15_7), + ".", + "-V", + "--xattr-template", + "keywords", + "{person}", + "--xattr-template", + "comment", + "{title}", + "--uuid", + f"{uuid}", + "--update", + ], + ) + assert result.exit_code == 0 + assert "Skipping extended attribute keywords" in result.output + assert "Skipping extended attribute comment" in result.output + + # clear tags and run again, should update extended attributes + md.keywords = None + md.comment = None + + result = runner.invoke( + export, + [ + os.path.join(cwd, PHOTOS_DB_15_7), + ".", + "-V", + "--xattr-template", + "keywords", + "{person}", + "--xattr-template", + "comment", + "{title}", + "--uuid", + f"{uuid}", + "--update", + ], + ) + assert result.exit_code == 0 + assert "Writing extended attribute keyword" in result.output + assert "Writing extended attribute comment" in result.output + + md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"]) + expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"] + expected = [expected] if type(expected) != list else expected + assert sorted(md.keywords) == sorted(expected) + assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"] +