From 6879cf950d02032ac3d4baa850cf51774d0941ff Mon Sep 17 00:00:00 2001 From: kikuyan Date: Fri, 25 Jun 2021 13:51:14 +0900 Subject: [PATCH] Write --xattrs metadata as macOS Spotlight metadata --- youtube_dl/options.py | 2 +- youtube_dl/postprocessor/xattrpp.py | 105 ++++++++++++++++++++++++---- youtube_dl/utils.py | 47 ++++++++++++- 3 files changed, 140 insertions(+), 14 deletions(-) diff --git a/youtube_dl/options.py b/youtube_dl/options.py index 0a0641bd4..1c4f668ee 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -835,7 +835,7 @@ def parseOpts(overrideArguments=None): postproc.add_option( '--xattrs', action='store_true', dest='xattrs', default=False, - help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)') + help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards, or macOS Spotlight)') postproc.add_option( '--fixup', metavar='POLICY', dest='fixup', default='detect_or_warn', diff --git a/youtube_dl/postprocessor/xattrpp.py b/youtube_dl/postprocessor/xattrpp.py index 814dabecf..04c3b8f0f 100644 --- a/youtube_dl/postprocessor/xattrpp.py +++ b/youtube_dl/postprocessor/xattrpp.py @@ -1,8 +1,17 @@ from __future__ import unicode_literals +import plistlib +import subprocess +import sys + +from xml.sax.saxutils import escape + from .common import PostProcessor from ..compat import compat_os_name from ..utils import ( + check_executable, + encodeArgument, + encodeFilename, hyphenate_date, write_xattr, XAttrMetadataError, @@ -32,15 +41,26 @@ class XAttrMetadataPP(PostProcessor): filename = info['filepath'] try: - xattr_mapping = { - 'user.xdg.referrer.url': 'webpage_url', - # 'user.xdg.comment': 'description', - 'user.dublincore.title': 'title', - 'user.dublincore.date': 'upload_date', - 'user.dublincore.description': 'description', - 'user.dublincore.contributor': 'uploader', - 'user.dublincore.format': 'format', - } + if sys.platform != 'darwin': # other than macOS + xattr_mapping = { + 'user.xdg.referrer.url': 'webpage_url', + # 'user.xdg.comment': 'description', + 'user.dublincore.title': 'title', + 'user.dublincore.date': 'upload_date', + 'user.dublincore.description': 'description', + 'user.dublincore.contributor': 'uploader', + 'user.dublincore.format': 'format', + } + else: # macOS + xattr_mapping = { + 'com.apple.metadata:kMDItemWhereFroms': 'webpage_url', + # 'user.xdg.comment': 'description', + 'com.apple.metadata:kMDItemTitle': 'title', + 'user.dublincore.date': 'upload_date', # no corresponding attr + 'com.apple.metadata:kMDItemDescription': 'description', + 'com.apple.metadata:kMDItemContributors': 'uploader', + 'user.dublincore.format': 'format', # no corresponding attr + } num_written = 0 for xattrname, infoname in xattr_mapping.items(): @@ -48,10 +68,15 @@ class XAttrMetadataPP(PostProcessor): value = info.get(infoname) if value: - if infoname == 'upload_date': - value = hyphenate_date(value) + if not xattrname.startswith('com.apple.metadata:'): + if infoname == 'upload_date': + value = hyphenate_date(value) + + byte_value = value.encode('utf-8') + + else: # macOS Spotlight metadata + byte_value = self.make_mditem(xattrname, value) - byte_value = value.encode('utf-8') write_xattr(filename, xattrname, byte_value) num_written += 1 @@ -77,3 +102,59 @@ class XAttrMetadataPP(PostProcessor): msg += '(You may have to enable them in your /etc/fstab)' self._downloader.report_error(msg) return [], info + + def make_mditem(self, attrname, value): + # Info about macOS Spotlight metadata: + # https://developer.apple.com/library/archive/documentation/CoreServices/Reference/MetadataAttributesRef/Reference/CommonAttrs.html + + attr_is_cfarray = attrname in ( + 'com.apple.metadata:kMDItemContributors', + 'com.apple.metadata:kMDItemWhereFroms') + + if hasattr(plistlib, 'dumps'): # Python >= 3.4, need new api to make binary plist + if attr_is_cfarray: + value = [value] + return plistlib.dumps(value, fmt=plistlib.FMT_BINARY) + + else: + # try PyObjC (or pyobjc-framework-Cocoa) + try: + from Foundation import NSPropertyListSerialization, NSPropertyListBinaryFormat_v1_0 + + if attr_is_cfarray: + data = [value] + else: + data = value + plist, err = NSPropertyListSerialization.dataWithPropertyList_format_options_error_( + data, NSPropertyListBinaryFormat_v1_0, 0, None) + if not err and plist: + return bytes(plist) + except (ImportError, ValueError): + pass # go on to try plutil command + + # make xml plist first to convert to binary plist with plutil command, + # or to use as a fallback if conversion failed + plist = '' + escape(value) + '\n' + if attr_is_cfarray: + plist = '\n\t' + plist + '\n' + plist = ( + '\n' + '\n' + '\n') + plist + '' + xmlplist = plist.encode('utf-8') + + # try plutil command (like `cat xmlplist | plutil -convert binary1 -o - -`) + plutil = check_executable('plutil', ['-help']) + if plutil: + cmd = ([encodeFilename(plutil, True)] + + [encodeArgument(o) for o in ['-convert', 'binary1', '-o', '-', '-']]) + try: + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + stdout, stderr = p.communicate(input=xmlplist) + if p.returncode == 0: + return bytes(stdout) + except EnvironmentError: + pass # fallback to xml plist + + return xmlplist diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index e722eed58..1ad5e8edb 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -5702,7 +5702,9 @@ def write_xattr(path, key, value): f.write(value) except EnvironmentError as e: raise XAttrMetadataError(e.errno, e.strerror) - else: + elif not (key.startswith('com.apple.metadata:') and value[:8] == b'bplist00'): + # other than macOS binary plist + user_has_setfattr = check_executable('setfattr', ['--version']) user_has_xattr = check_executable('xattr', ['-h']) @@ -5743,6 +5745,49 @@ def write_xattr(path, key, value): "Couldn't find a tool to set the xattrs. " "Install either the python 'xattr' module, " "or the 'xattr' binary.") + else: + # macOS binary plist + + # find Apple version xattr command to set binary data in hex string + # original xattr project's xattr command doesn't have this feature + xattr_bin = None + for _bin in ('xattr', '/usr/bin/xattr'): + cmd = [encodeFilename(_bin, True), encodeArgument('-h')] + try: + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except EnvironmentError: + continue + stdout, stderr = p.communicate() + if p.returncode != 0: + continue + stdout = stdout.decode('utf-8', 'replace') + # help text must contain '-x: ... hex string for input' line + if re.search('-x: .*? hex string for input', stdout): + xattr_bin = _bin + break + + if xattr_bin: + hexvalue = binascii.hexlify(value) + opts = ['-w', '-x', key, hexvalue] + cmd = ([encodeFilename(xattr_bin, True)] + + [encodeArgument(o) for o in opts] + + [encodeFilename(path, True)]) + try: + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except EnvironmentError as e: + raise XAttrMetadataError(e.errno, e.strerror) + stdout, stderr = p.communicate() + if p.returncode != 0: + stderr = stderr.decode('utf-8', 'replace') + raise XAttrMetadataError(p.returncode, stderr) + + else: + raise XAttrUnavailableError( + "Couldn't find a tool to set the xattrs. " + "Install either the python 'xattr' module, " + "or the Apple version 'xattr' command.") def random_birthday(year_field, month_field, day_field):