]>
Commit | Line | Data |
---|---|---|
dcddc10a PH |
1 | from __future__ import unicode_literals |
2 | ||
496c1923 PH |
3 | import os |
4 | import subprocess | |
5 | import sys | |
86c7fdb1 | 6 | import errno |
496c1923 PH |
7 | |
8 | from .common import PostProcessor | |
e9c0cdd3 | 9 | from ..compat import compat_os_name |
496c1923 | 10 | from ..utils import ( |
168da92b | 11 | check_executable, |
496c1923 | 12 | hyphenate_date, |
8c882617 | 13 | version_tuple, |
86c7fdb1 YCH |
14 | PostProcessingError, |
15 | encodeArgument, | |
16 | encodeFilename, | |
496c1923 PH |
17 | ) |
18 | ||
19 | ||
86c7fdb1 YCH |
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' | |
fbff30d2 YCH |
29 | elif self.code == errno.E2BIG or 'Argument list too long' in self.msg: |
30 | self.reason = 'VALUE_TOO_LONG' | |
86c7fdb1 YCH |
31 | else: |
32 | self.reason = 'NOT_SUPPORTED' | |
33 | ||
34 | ||
496c1923 PH |
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 | |
168da92b | 56 | |
8c882617 YCH |
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. ' | |
4515cb43 | 64 | 'youtube-dl requires %s or above while your version is %s. ' |
8c882617 YCH |
65 | 'Falling back to other xattr implementations' % ( |
66 | pyxattr_required_version, xattr.__version__)) | |
67 | ||
68 | raise ImportError | |
69 | ||
496c1923 | 70 | def write_xattr(path, key, value): |
86c7fdb1 YCH |
71 | try: |
72 | xattr.set(path, key, value) | |
73 | except EnvironmentError as e: | |
74 | raise XAttrMetadataError(e.errno, e.strerror) | |
496c1923 PH |
75 | |
76 | except ImportError: | |
e9c0cdd3 | 77 | if compat_os_name == 'nt': |
168da92b PH |
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): | |
a9ce0c63 PH |
81 | assert ':' not in key |
82 | assert os.path.exists(path) | |
496c1923 | 83 | |
611c1dd9 | 84 | ads_fn = path + ':' + key |
86c7fdb1 | 85 | try: |
611c1dd9 | 86 | with open(ads_fn, 'wb') as f: |
86c7fdb1 YCH |
87 | f.write(value) |
88 | except EnvironmentError as e: | |
89 | raise XAttrMetadataError(e.errno, e.strerror) | |
168da92b | 90 | else: |
611c1dd9 S |
91 | user_has_setfattr = check_executable('setfattr', ['--version']) |
92 | user_has_xattr = check_executable('xattr', ['-h']) | |
496c1923 PH |
93 | |
94 | if user_has_setfattr or user_has_xattr: | |
95 | ||
96 | def write_xattr(path, key, value): | |
86c7fdb1 | 97 | value = value.decode('utf-8') |
496c1923 | 98 | if user_has_setfattr: |
86c7fdb1 YCH |
99 | executable = 'setfattr' |
100 | opts = ['-n', key, '-v', value] | |
496c1923 | 101 | elif user_has_xattr: |
86c7fdb1 YCH |
102 | executable = 'xattr' |
103 | opts = ['-w', key, value] | |
496c1923 | 104 | |
86c7fdb1 YCH |
105 | cmd = ([encodeFilename(executable, True)] + |
106 | [encodeArgument(o) for o in opts] + | |
107 | [encodeFilename(path, True)]) | |
108 | ||
fbff30d2 YCH |
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) | |
86c7fdb1 YCH |
114 | stdout, stderr = p.communicate() |
115 | stderr = stderr.decode('utf-8', 'replace') | |
116 | if p.returncode != 0: | |
117 | raise XAttrMetadataError(p.returncode, stderr) | |
496c1923 PH |
118 | |
119 | else: | |
120 | # On Unix, and can't find pyxattr, setfattr, or xattr. | |
121 | if sys.platform.startswith('linux'): | |
2a2e2770 PH |
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.") | |
496c1923 PH |
132 | |
133 | # Write the metadata to the file's xattrs | |
168da92b | 134 | self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs') |
496c1923 PH |
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: | |
611c1dd9 | 154 | if infoname == 'upload_date': |
496c1923 PH |
155 | value = hyphenate_date(value) |
156 | ||
afc7bc33 | 157 | byte_value = value.encode('utf-8') |
42cc71e8 | 158 | write_xattr(filename, xattrname, byte_value) |
496c1923 | 159 | |
592e97e8 | 160 | return [], info |
496c1923 | 161 | |
86c7fdb1 YCH |
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.') | |
fbff30d2 YCH |
167 | elif e.reason == 'VALUE_TOO_LONG': |
168 | self._downloader.report_warning( | |
169 | 'Unable to write extended attributes due to too long values.') | |
86c7fdb1 | 170 | else: |
c827d4cf | 171 | msg = 'This filesystem doesn\'t support extended attributes. ' |
e9c0cdd3 | 172 | if compat_os_name == 'nt': |
c827d4cf YCH |
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) | |
592e97e8 | 177 | return [], info |