aboutsummaryrefslogtreecommitdiffstats
path: root/youtube_dl/postprocessor/xattrpp.py
blob: 27e273000a5171194c24d8c1e1155c157bc0f387 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from __future__ import unicode_literals

import os
import subprocess
import sys
import errno

from .common import PostProcessor
from ..utils import (
    check_executable,
    hyphenate_date,
    version_tuple,
    PostProcessingError,
    encodeArgument,
    encodeFilename,
)


class XAttrMetadataError(PostProcessingError):
    def __init__(self, code=None, msg='Unknown error'):
        super(XAttrMetadataError, self).__init__(msg)
        self.code = code

        # Parsing code and msg
        if (self.code in (errno.ENOSPC, errno.EDQUOT) or
                'No space left' in self.msg or 'Disk quota excedded' in self.msg):
            self.reason = 'NO_SPACE'
        elif self.code == errno.E2BIG or 'Argument list too long' in self.msg:
            self.reason = 'VALUE_TOO_LONG'
        else:
            self.reason = 'NOT_SUPPORTED'


class XAttrMetadataPP(PostProcessor):

    #
    # More info about extended attributes for media:
    #   http://freedesktop.org/wiki/CommonExtendedAttributes/
    #   http://www.freedesktop.org/wiki/PhreedomDraft/
    #   http://dublincore.org/documents/usageguide/elements.shtml
    #
    # TODO:
    #  * capture youtube keywords and put them in 'user.dublincore.subject' (comma-separated)
    #  * figure out which xattrs can be used for 'duration', 'thumbnail', 'resolution'
    #

    def run(self, info):
        """ Set extended attributes on downloaded file (if xattr support is found). """

        # This mess below finds the best xattr tool for the job and creates a
        # "write_xattr" function.
        try:
            # try the pyxattr module...
            import xattr

            # Unicode arguments are not supported in python-pyxattr until
            # version 0.5.0
            # See https://github.com/rg3/youtube-dl/issues/5498
            pyxattr_required_version = '0.5.0'
            if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
                self._downloader.report_warning(
                    'python-pyxattr is detected but is too old. '
                    'youtube-dl requires %s or above while your version is %s. '
                    'Falling back to other xattr implementations' % (
                        pyxattr_required_version, xattr.__version__))

                raise ImportError

            def write_xattr(path, key, value):
                try:
                    xattr.set(path, key, value)
                except EnvironmentError as e:
                    raise XAttrMetadataError(e.errno, e.strerror)

        except ImportError:
            if os.name == 'nt':
                # Write xattrs to NTFS Alternate Data Streams:
                # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
                def write_xattr(path, key, value):
                    assert ':' not in key
                    assert os.path.exists(path)

                    ads_fn = path + ":" + key
                    try:
                        with open(ads_fn, "wb") as f:
                            f.write(value)
                    except EnvironmentError as e:
                        raise XAttrMetadataError(e.errno, e.strerror)
            else:
                user_has_setfattr = check_executable("setfattr", ['--version'])
                user_has_xattr = check_executable("xattr", ['-h'])

                if user_has_setfattr or user_has_xattr:

                    def write_xattr(path, key, value):
                        value = value.decode('utf-8')
                        if user_has_setfattr:
                            executable = 'setfattr'
                            opts = ['-n', key, '-v', value]
                        elif user_has_xattr:
                            executable = 'xattr'
                            opts = ['-w', key, value]

                        cmd = ([encodeFilename(executable, True)] +
                               [encodeArgument(o) for o in opts] +
                               [encodeFilename(path, True)])

                        try:
                            p = subprocess.Popen(
                                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
                        except EnvironmentError as e:
                            raise XAttrMetadataError(e.errno, e.strerror)
                        stdout, stderr = p.communicate()
                        stderr = stderr.decode('utf-8', 'replace')
                        if p.returncode != 0:
                            raise XAttrMetadataError(p.returncode, stderr)

                else:
                    # On Unix, and can't find pyxattr, setfattr, or xattr.
                    if sys.platform.startswith('linux'):
                        self._downloader.report_error(
                            "Couldn't find a tool to set the xattrs. "
                            "Install either the python 'pyxattr' or 'xattr' "
                            "modules, or the GNU 'attr' package "
                            "(which contains the 'setfattr' tool).")
                    else:
                        self._downloader.report_error(
                            "Couldn't find a tool to set the xattrs. "
                            "Install either the python 'xattr' module, "
                            "or the 'xattr' binary.")

        # Write the metadata to the file's xattrs
        self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs')

        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',
            }

            for xattrname, infoname in xattr_mapping.items():

                value = info.get(infoname)

                if value:
                    if infoname == "upload_date":
                        value = hyphenate_date(value)

                    byte_value = value.encode('utf-8')
                    write_xattr(filename, xattrname, byte_value)

            return [], info

        except XAttrMetadataError as e:
            if e.reason == 'NO_SPACE':
                self._downloader.report_warning(
                    'There\'s no disk space left or disk quota exceeded. ' +
                    'Extended attributes are not written.')
            elif e.reason == 'VALUE_TOO_LONG':
                self._downloader.report_warning(
                    'Unable to write extended attributes due to too long values.')
            else:
                self._downloader.report_error(
                    'This filesystem doesn\'t support extended attributes. ' +
                    '(You may have to enable them in your /etc/fstab)')
            return [], info