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