From 2bccca2f6cc81c7b3070dabbf1733f3a8e4a87ab Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Wed, 16 Nov 2022 06:45:31 -0800 Subject: [PATCH] Fix for kMDItemKeywords as comma delimited string, #83, #84 --- osxmetadata/__main__.py | 11 +++- osxmetadata/mditem.py | 14 +++-- osxmetadata/osxmetadata.py | 35 ++++++++++++- tests/test_cli.py | 90 +++++++++++++++++++++++++++++++++ tests/test_mditem_attributes.py | 10 ++++ 5 files changed, 154 insertions(+), 6 deletions(-) diff --git a/osxmetadata/__main__.py b/osxmetadata/__main__.py index 545c6fd..46456df 100644 --- a/osxmetadata/__main__.py +++ b/osxmetadata/__main__.py @@ -745,7 +745,11 @@ def get_help(self, ctx): "-s", "set_", metavar="ATTRIBUTE VALUE", - help="Set ATTRIBUTE to VALUE.", + help="Set ATTRIBUTE to VALUE. " + "If ATTRIBUTE is a multi-value attribute, such as keywords (kMDItemKeywords), " + "you may specify --set multiple times to add to the array of values: " + "'--set keywords foo --set keywords bar' will set keywords to ['foo', 'bar']. " + "Not that this will overwrite any existing values for the attribute; see also --append.", nargs=2, multiple=True, required=False, @@ -788,7 +792,10 @@ def get_help(self, ctx): "--append", "-a", metavar="ATTRIBUTE VALUE", - help="Append VALUE to ATTRIBUTE; for multi-valued attributes, appends only if VALUE is not already present.", + help="Append VALUE to ATTRIBUTE; for multi-valued attributes, appends only if VALUE is not already present. " + "May be used in combination with --set to add to an existing value: " + "'--set keywords foo --append keywords bar' will set keywords to ['foo', 'bar'], " + "overwriting any existing values for the attribute.", nargs=2, multiple=True, required=False, diff --git a/osxmetadata/mditem.py b/osxmetadata/mditem.py index 328e2a7..b5049af 100644 --- a/osxmetadata/mditem.py +++ b/osxmetadata/mditem.py @@ -51,7 +51,7 @@ # appropriate Objective C object pointers. -def MDItemSetAttribute(mditem, name, attr): +def MDItemSetAttribute(mditem, name, attr) -> bool: """dummy function definition""" ... @@ -100,7 +100,7 @@ def set_mditem_metadata( ) -> bool: """Set file metadata using undocumented function MDItemSetAttribute - file: path to file + mditem: MDItem object attribute: metadata attribute to set value: value to set attribute to; must match the type expected by the attribute (e.g. bool, str, List[str], float, datetime.datetime) @@ -149,7 +149,13 @@ def get_mditem_metadata( elif attribute_type == "float": return float(value) elif attribute_type == "list": - return [str(x) for x in value] + # some attributes like kMDItemKeywords do not always follow the documented type + # and can return a single comma-delimited string instead of a list (See #83) + return ( + str(value).split(",") + if isinstance(value, (objc.pyobjc_unicode, str)) + else [str(x) for x in value] + ) elif attribute_type == "datetime.datetime": return CFDate_to_datetime(value) elif attribute_type == "list[datetime.datetime]": @@ -160,6 +166,8 @@ def get_mditem_metadata( elif "__NSTaggedDate" in repr(type(value)): # this is a hack but works for MDImporter attributes that don't have a documented type return NSDate_to_datetime(value) + elif isinstance(value, objc.pyobjc_unicode): + return str(value) else: return value except ValueError: diff --git a/osxmetadata/osxmetadata.py b/osxmetadata/osxmetadata.py index b714d38..7df8160 100644 --- a/osxmetadata/osxmetadata.py +++ b/osxmetadata/osxmetadata.py @@ -31,7 +31,12 @@ set_finderinfo_stationerypad, ) from .finder_tags import _kMDItemUserTags, get_finder_tags, set_finder_tags -from .mditem import MDItemValueType, get_mditem_metadata, set_or_remove_mditem_metadata +from .mditem import ( + MDItemValueType, + get_mditem_metadata, + set_or_remove_mditem_metadata, + MDItemSetAttribute, +) from .nsurl_metadata import get_nsurl_metadata, set_nsurl_metadata ALL_ATTRIBUTES = { @@ -197,6 +202,34 @@ def to_json( return json.dumps(dict_data, indent=indent) + def get_mditem_attribute_value(self, attribute: str) -> t.Any: + """Get the raw MDItem attribute value without any type conversion. + + Args: + attribute: metadata attribute name + + Returns: + raw MDItem attribute value as returned by CoreServices.MDItemCopyAttribute() + + Note: This is a low level function that you probably don't need to use, + but may be useful in some cases. You should probably use the get() method instead. + """ + return CoreServices.MDItemCopyAttribute(self._mditem, attribute) + + def set_mditem_attribute_value(self, attribute: str, value: t.Any) -> bool: + """Set the raw MDItem attribute value without any type conversion. + + Args: + attribute: metadata attribute name + value: value to set attribute to + + Returns: True if successful otherwise False + + Note: This is a low level function that you probably don't need to use, + but may be useful in some cases. You should probably use the set() method instead. + """ + return MDItemSetAttribute(self._mditem, attribute, value) + @property def path(self) -> str: """Return path to file""" diff --git a/tests/test_cli.py b/tests/test_cli.py index eeeaa9a..6052f30 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -109,6 +109,50 @@ def test_cli_set(test_file): assert md.description == "Goodbye World" +def test_cli_set_multi_keywords_1(test_file): + """Test --set with multiple keywords (#83)""" + + runner = CliRunner() + result = runner.invoke( + cli, + [ + "--set", + "keywords", + "Foo", + "--set", + "keywords", + "Bar", + test_file.name, + ], + ) + snooze() + assert result.exit_code == 0 + md = OSXMetaData(test_file.name) + assert sorted(md.keywords) == ["Bar", "Foo"] + + +def test_cli_set_multi_keywords_2(test_file): + """Test --set, --append with multiple keywords (#83)""" + + runner = CliRunner() + result = runner.invoke( + cli, + [ + "--set", + "keywords", + "Foo", + "--append", + "keywords", + "Bar", + test_file.name, + ], + ) + snooze() + assert result.exit_code == 0 + md = OSXMetaData(test_file.name) + assert sorted(md.keywords) == ["Bar", "Foo"] + + def test_cli_clear(test_file): """Test --clear""" @@ -151,6 +195,52 @@ def test_cli_append(test_file): assert md.tags == [Tag("test", 0)] +def test_cli_set_then_append(test_file): + """Test --set then --append""" + + md = OSXMetaData(test_file.name) + md.authors = ["John Doe"] + + # set initial value + runner = CliRunner() + result = runner.invoke( + cli, + [ + "--set", + "keywords", + "foo", + test_file.name, + ], + ) + assert result.exit_code == 0 + + # set again and verify that it overwrites + result = runner.invoke( + cli, + [ + "--set", + "keywords", + "bar", + test_file.name, + ], + ) + assert result.exit_code == 0 + assert md.keywords == ["bar"] + + # append and verify that it appends + result = runner.invoke( + cli, + [ + "--append", + "keywords", + "baz", + test_file.name, + ], + ) + assert result.exit_code == 0 + assert sorted(md.keywords) == ["bar", "baz"] + + def test_cli_get(test_file): """Test --get""" diff --git a/tests/test_mditem_attributes.py b/tests/test_mditem_attributes.py index ea09d7b..87f0b33 100644 --- a/tests/test_mditem_attributes.py +++ b/tests/test_mditem_attributes.py @@ -163,3 +163,13 @@ def test_mditem_attributes_audio(test_audio): md = OSXMetaData(test_audio) assert md.get("kMDItemAudioSampleRate") == 44100.0 + + +def test_get_set_mditem_attribute_value(test_file): + """test get and set of mditem attribute value using the direct methods without value conversion, #83""" + + md = OSXMetaData(test_file.name) + md.set_mditem_attribute_value("kMDItemComment", "foo,bar") + snooze() + assert md.get_mditem_attribute_value("kMDItemComment") == "foo,bar" + assert md.comment == "foo,bar"