]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/common.py
Improved progress reporting (See desc) (#1125)
[yt-dlp.git] / yt_dlp / postprocessor / common.py
CommitLineData
dcddc10a
PH
1from __future__ import unicode_literals
2
819e0531 3import copy
8326b00a 4import functools
dd29eb7f
S
5import os
6
43820c03 7from ..compat import compat_str
dd29eb7f 8from ..utils import (
330690a2 9 _configuration_args,
dd29eb7f 10 encodeFilename,
eab9b2bc 11 PostProcessingError,
dd29eb7f 12)
496c1923
PH
13
14
819e0531 15class PostProcessorMetaClass(type):
16 @staticmethod
17 def run_wrapper(func):
18 @functools.wraps(func)
19 def run(self, info, *args, **kwargs):
20 self._hook_progress({'status': 'started'}, info)
21 ret = func(self, info, *args, **kwargs)
22 if ret is not None:
23 _, info = ret
24 self._hook_progress({'status': 'finished'}, info)
25 return ret
26 return run
27
28 def __new__(cls, name, bases, attrs):
29 if 'run' in attrs:
30 attrs['run'] = cls.run_wrapper(attrs['run'])
31 return type.__new__(cls, name, bases, attrs)
32
33
34class PostProcessor(metaclass=PostProcessorMetaClass):
496c1923
PH
35 """Post Processor class.
36
37 PostProcessor objects can be added to downloaders with their
38 add_post_processor() method. When the downloader has finished a
39 successful download, it will take its internal chain of PostProcessors
40 and start calling the run() method on each one of them, first with
41 an initial argument and then with the returned value of the previous
42 PostProcessor.
43
44 The chain will be stopped if one of them ever returns None or the end
45 of the chain is reached.
46
47 PostProcessor objects follow a "mutual registration" process similar
e35b23f5
S
48 to InfoExtractor objects.
49
50 Optionally PostProcessor can use a list of additional command-line arguments
51 with self._configuration_args.
496c1923
PH
52 """
53
54 _downloader = None
55
aa5d9a79 56 def __init__(self, downloader=None):
819e0531 57 self._progress_hooks = []
58 self.add_progress_hook(self.report_progress)
59 self.set_downloader(downloader)
43820c03 60 self.PP_NAME = self.pp_key()
61
62 @classmethod
63 def pp_key(cls):
64 name = cls.__name__[:-2]
65 return compat_str(name[6:]) if name[:6].lower() == 'ffmpeg' else name
1b77b347 66
fbced734 67 def to_screen(self, text, prefix=True, *args, **kwargs):
68 tag = '[%s] ' % self.PP_NAME if prefix else ''
f446cc66 69 if self._downloader:
fbced734 70 return self._downloader.to_screen('%s%s' % (tag, text), *args, **kwargs)
f446cc66 71
72 def report_warning(self, text, *args, **kwargs):
73 if self._downloader:
74 return self._downloader.report_warning(text, *args, **kwargs)
75
76 def report_error(self, text, *args, **kwargs):
b1940459 77 # Exists only for compatibility. Do not use
f446cc66 78 if self._downloader:
79 return self._downloader.report_error(text, *args, **kwargs)
80
0760b0a7 81 def write_debug(self, text, *args, **kwargs):
82 if self._downloader:
83 return self._downloader.write_debug(text, *args, **kwargs)
f446cc66 84
85 def get_param(self, name, default=None, *args, **kwargs):
86 if self._downloader:
87 return self._downloader.params.get(name, default, *args, **kwargs)
88 return default
496c1923
PH
89
90 def set_downloader(self, downloader):
91 """Sets the downloader for this PP."""
92 self._downloader = downloader
819e0531 93 if not downloader:
94 return
95 for ph in downloader._postprocessor_hooks:
96 self.add_progress_hook(ph)
496c1923 97
8326b00a 98 @staticmethod
99 def _restrict_to(*, video=True, audio=True, images=True):
100 allowed = {'video': video, 'audio': audio, 'images': images}
101
102 def decorator(func):
103 @functools.wraps(func)
104 def wrapper(self, info):
105 format_type = (
7e87e27c 106 'video' if info.get('vcodec') != 'none'
107 else 'audio' if info.get('acodec') != 'none'
8326b00a 108 else 'images')
109 if allowed[format_type]:
4d85fbbd 110 return func(self, info)
8326b00a 111 else:
112 self.to_screen('Skipping %s' % format_type)
113 return [], info
114 return wrapper
115 return decorator
116
496c1923
PH
117 def run(self, information):
118 """Run the PostProcessor.
119
120 The "information" argument is a dictionary like the ones
121 composed by InfoExtractors. The only difference is that this
122 one has an extra field called "filepath" that points to the
123 downloaded file.
124
592e97e8
JMF
125 This method returns a tuple, the first element is a list of the files
126 that can be deleted, and the second of which is the updated
127 information.
496c1923
PH
128
129 In addition, this method may raise a PostProcessingError
130 exception if post processing fails.
131 """
592e97e8 132 return [], information # by default, keep file and do nothing
496c1923 133
dd29eb7f
S
134 def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
135 try:
136 os.utime(encodeFilename(path), (atime, mtime))
137 except Exception:
f446cc66 138 self.report_warning(errnote)
dd29eb7f 139
330690a2 140 def _configuration_args(self, exe, *args, **kwargs):
141 return _configuration_args(
142 self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
e35b23f5 143
819e0531 144 def _hook_progress(self, status, info_dict):
145 if not self._progress_hooks:
146 return
147 info_dict = dict(info_dict)
148 for key in ('__original_infodict', '__postprocessors'):
149 info_dict.pop(key, None)
150 status.update({
151 'info_dict': copy.deepcopy(info_dict),
152 'postprocessor': self.pp_key(),
153 })
154 for ph in self._progress_hooks:
155 ph(status)
156
157 def add_progress_hook(self, ph):
158 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
159 self._progress_hooks.append(ph)
160
161 def report_progress(self, s):
162 s['_default_template'] = '%(postprocessor)s %(status)s' % s
163
164 progress_dict = s.copy()
165 progress_dict.pop('info_dict')
166 progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
167
168 progress_template = self.get_param('progress_template', {})
169 tmpl = progress_template.get('postprocess')
170 if tmpl:
171 self._downloader.to_stdout(self._downloader.evaluate_outtmpl(tmpl, progress_dict))
172
173 self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
174 progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
175 progress_dict))
176
496c1923
PH
177
178class AudioConversionError(PostProcessingError):
179 pass