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