xattrpp.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. from __future__ import unicode_literals
  2. import os
  3. import subprocess
  4. import sys
  5. import errno
  6. from .common import PostProcessor
  7. from ..utils import (
  8. check_executable,
  9. hyphenate_date,
  10. version_tuple,
  11. PostProcessingError,
  12. encodeArgument,
  13. encodeFilename,
  14. )
  15. class XAttrMetadataError(PostProcessingError):
  16. def __init__(self, code=None, msg='Unknown error'):
  17. super(XAttrMetadataError, self).__init__(msg)
  18. self.code = code
  19. # Parsing code and msg
  20. if (self.code in (errno.ENOSPC, errno.EDQUOT) or
  21. 'No space left' in self.msg or 'Disk quota excedded' in self.msg):
  22. self.reason = 'NO_SPACE'
  23. else:
  24. self.reason = 'NOT_SUPPORTED'
  25. class XAttrMetadataPP(PostProcessor):
  26. #
  27. # More info about extended attributes for media:
  28. # http://freedesktop.org/wiki/CommonExtendedAttributes/
  29. # http://www.freedesktop.org/wiki/PhreedomDraft/
  30. # http://dublincore.org/documents/usageguide/elements.shtml
  31. #
  32. # TODO:
  33. # * capture youtube keywords and put them in 'user.dublincore.subject' (comma-separated)
  34. # * figure out which xattrs can be used for 'duration', 'thumbnail', 'resolution'
  35. #
  36. def run(self, info):
  37. """ Set extended attributes on downloaded file (if xattr support is found). """
  38. # This mess below finds the best xattr tool for the job and creates a
  39. # "write_xattr" function.
  40. try:
  41. # try the pyxattr module...
  42. import xattr
  43. # Unicode arguments are not supported in python-pyxattr until
  44. # version 0.5.0
  45. # See https://github.com/rg3/youtube-dl/issues/5498
  46. pyxattr_required_version = '0.5.0'
  47. if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
  48. self._downloader.report_warning(
  49. 'python-pyxattr is detected but is too old. '
  50. 'youtube-dl requires %s or above while your version is %s. '
  51. 'Falling back to other xattr implementations' % (
  52. pyxattr_required_version, xattr.__version__))
  53. raise ImportError
  54. def write_xattr(path, key, value):
  55. try:
  56. xattr.set(path, key, value)
  57. except EnvironmentError as e:
  58. raise XAttrMetadataError(e.errno, e.strerror)
  59. except ImportError:
  60. if os.name == 'nt':
  61. # Write xattrs to NTFS Alternate Data Streams:
  62. # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
  63. def write_xattr(path, key, value):
  64. assert ':' not in key
  65. assert os.path.exists(path)
  66. ads_fn = path + ":" + key
  67. try:
  68. with open(ads_fn, "wb") as f:
  69. f.write(value)
  70. except EnvironmentError as e:
  71. raise XAttrMetadataError(e.errno, e.strerror)
  72. else:
  73. user_has_setfattr = check_executable("setfattr", ['--version'])
  74. user_has_xattr = check_executable("xattr", ['-h'])
  75. if user_has_setfattr or user_has_xattr:
  76. def write_xattr(path, key, value):
  77. value = value.decode('utf-8')
  78. if user_has_setfattr:
  79. executable = 'setfattr'
  80. opts = ['-n', key, '-v', value]
  81. elif user_has_xattr:
  82. executable = 'xattr'
  83. opts = ['-w', key, value]
  84. cmd = ([encodeFilename(executable, True)] +
  85. [encodeArgument(o) for o in opts] +
  86. [encodeFilename(path, True)])
  87. p = subprocess.Popen(
  88. cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
  89. stdout, stderr = p.communicate()
  90. stderr = stderr.decode('utf-8', 'replace')
  91. if p.returncode != 0:
  92. raise XAttrMetadataError(p.returncode, stderr)
  93. else:
  94. # On Unix, and can't find pyxattr, setfattr, or xattr.
  95. if sys.platform.startswith('linux'):
  96. self._downloader.report_error(
  97. "Couldn't find a tool to set the xattrs. "
  98. "Install either the python 'pyxattr' or 'xattr' "
  99. "modules, or the GNU 'attr' package "
  100. "(which contains the 'setfattr' tool).")
  101. else:
  102. self._downloader.report_error(
  103. "Couldn't find a tool to set the xattrs. "
  104. "Install either the python 'xattr' module, "
  105. "or the 'xattr' binary.")
  106. # Write the metadata to the file's xattrs
  107. self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs')
  108. filename = info['filepath']
  109. try:
  110. xattr_mapping = {
  111. 'user.xdg.referrer.url': 'webpage_url',
  112. # 'user.xdg.comment': 'description',
  113. 'user.dublincore.title': 'title',
  114. 'user.dublincore.date': 'upload_date',
  115. 'user.dublincore.description': 'description',
  116. 'user.dublincore.contributor': 'uploader',
  117. 'user.dublincore.format': 'format',
  118. }
  119. for xattrname, infoname in xattr_mapping.items():
  120. value = info.get(infoname)
  121. if value:
  122. if infoname == "upload_date":
  123. value = hyphenate_date(value)
  124. byte_value = value.encode('utf-8')
  125. write_xattr(filename, xattrname, byte_value)
  126. return [], info
  127. except XAttrMetadataError as e:
  128. if e.reason == 'NO_SPACE':
  129. self._downloader.report_warning(
  130. 'There\'s no disk space left or disk quota exceeded. ' +
  131. 'Extended attributes are not written.')
  132. else:
  133. self._downloader.report_error(
  134. 'This filesystem doesn\'t support extended attributes. ' +
  135. '(You may have to enable them in your /etc/fstab)')
  136. return [], info