18 class PostProcessorMetaClass(type):
20 def run_wrapper(func
):
21 @functools.wraps(func
)
22 def run(self
, info
, *args
, **kwargs
):
23 info_copy
= self
._copy
_infodict
(info
)
24 self
._hook
_progress
({'status': 'started'}
, info_copy
)
25 ret
= func(self
, info
, *args
, **kwargs
)
28 self
._hook
_progress
({'status': 'finished'}
, info_copy
)
32 def __new__(cls
, name
, bases
, attrs
):
34 attrs
['run'] = cls
.run_wrapper(attrs
['run'])
35 return type.__new
__(cls
, name
, bases
, attrs
)
38 class PostProcessor(metaclass
=PostProcessorMetaClass
):
39 """Post Processor class.
41 PostProcessor objects can be added to downloaders with their
42 add_post_processor() method. When the downloader has finished a
43 successful download, it will take its internal chain of PostProcessors
44 and start calling the run() method on each one of them, first with
45 an initial argument and then with the returned value of the previous
48 The chain will be stopped if one of them ever returns None or the end
49 of the chain is reached.
51 PostProcessor objects follow a "mutual registration" process similar
52 to InfoExtractor objects.
54 Optionally PostProcessor can use a list of additional command-line arguments
55 with self._configuration_args.
60 def __init__(self
, downloader
=None):
61 self
._progress
_hooks
= []
62 self
.add_progress_hook(self
.report_progress
)
63 self
.set_downloader(downloader
)
64 self
.PP_NAME
= self
.pp_key()
68 name
= cls
.__name
__[:-2]
69 return name
[6:] if name
[:6].lower() == 'ffmpeg' else name
71 def to_screen(self
, text
, prefix
=True, *args
, **kwargs
):
72 tag
= '[%s] ' % self
.PP_NAME
if prefix
else ''
74 return self
._downloader
.to_screen(f
'{tag}{text}', *args
, **kwargs
)
76 def report_warning(self
, text
, *args
, **kwargs
):
78 return self
._downloader
.report_warning(text
, *args
, **kwargs
)
80 def deprecation_warning(self
, text
):
82 return self
._downloader
.deprecation_warning(text
)
83 write_string(f
'DeprecationWarning: {text}')
85 def report_error(self
, text
, *args
, **kwargs
):
86 # Exists only for compatibility. Do not use
88 return self
._downloader
.report_error(text
, *args
, **kwargs
)
90 def write_debug(self
, text
, *args
, **kwargs
):
92 return self
._downloader
.write_debug(text
, *args
, **kwargs
)
94 def get_param(self
, name
, default
=None, *args
, **kwargs
):
96 return self
._downloader
.params
.get(name
, default
, *args
, **kwargs
)
99 def set_downloader(self
, downloader
):
100 """Sets the downloader for this PP."""
101 self
._downloader
= downloader
102 for ph
in getattr(downloader
, '_postprocessor_hooks', []):
103 self
.add_progress_hook(ph
)
105 def _copy_infodict(self
, info_dict
):
106 return getattr(self
._downloader
, '_copy_infodict', dict)(info_dict
)
109 def _restrict_to(*, video
=True, audio
=True, images
=True, simulated
=True):
110 allowed
= {'video': video, 'audio': audio, 'images': images}
113 @functools.wraps(func
)
114 def wrapper(self
, info
):
115 if not simulated
and (self
.get_param('simulate') or self
.get_param('skip_download')):
118 'video' if info
.get('vcodec') != 'none'
119 else 'audio' if info
.get('acodec') != 'none'
121 if allowed
[format_type
]:
122 return func(self
, info
)
124 self
.to_screen('Skipping %s' % format_type
)
129 def run(self
, information
):
130 """Run the PostProcessor.
132 The "information" argument is a dictionary like the ones
133 composed by InfoExtractors. The only difference is that this
134 one has an extra field called "filepath" that points to the
137 This method returns a tuple, the first element is a list of the files
138 that can be deleted, and the second of which is the updated
141 In addition, this method may raise a PostProcessingError
142 exception if post processing fails.
144 return [], information
# by default, keep file and do nothing
146 def try_utime(self
, path
, atime
, mtime
, errnote
='Cannot update utime of file'):
148 os
.utime(encodeFilename(path
), (atime
, mtime
))
150 self
.report_warning(errnote
)
152 def _configuration_args(self
, exe
, *args
, **kwargs
):
153 return _configuration_args(
154 self
.pp_key(), self
.get_param('postprocessor_args'), exe
, *args
, **kwargs
)
156 def _hook_progress(self
, status
, info_dict
):
157 if not self
._progress
_hooks
:
160 'info_dict': info_dict
,
161 'postprocessor': self
.pp_key(),
163 for ph
in self
._progress
_hooks
:
166 def add_progress_hook(self
, ph
):
167 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
168 self
._progress
_hooks
.append(ph
)
170 def report_progress(self
, s
):
171 s
['_default_template'] = '%(postprocessor)s %(status)s' % s
173 progress_dict
= s
.copy()
174 progress_dict
.pop('info_dict')
175 progress_dict
= {'info': s['info_dict'], 'progress': progress_dict}
177 progress_template
= self
.get_param('progress_template', {})
178 tmpl
= progress_template
.get('postprocess')
180 self
._downloader
.to_stdout(self
._downloader
.evaluate_outtmpl(tmpl
, progress_dict
))
182 self
._downloader
.to_console_title(self
._downloader
.evaluate_outtmpl(
183 progress_template
.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
186 def _download_json(self
, url
, *, expected_http_errors
=(404,)):
187 # While this is not an extractor, it behaves similar to one and
188 # so obey extractor_retries and sleep_interval_requests
189 max_retries
= self
.get_param('extractor_retries', 3)
190 sleep_interval
= self
.get_param('sleep_interval_requests') or 0
192 self
.write_debug(f
'{self.PP_NAME} query: {url}')
193 for retries
in itertools
.count():
195 rsp
= self
._downloader
.urlopen(sanitized_Request(url
))
196 return json
.loads(rsp
.read().decode(rsp
.info().get_param('charset') or 'utf-8'))
197 except network_exceptions
as e
:
198 if isinstance(e
, urllib
.error
.HTTPError
) and e
.code
in expected_http_errors
:
200 if retries
< max_retries
:
201 self
.report_warning(f
'{e}. Retrying...')
202 if sleep_interval
> 0:
203 self
.to_screen(f
'Sleeping {sleep_interval} seconds ...')
204 time
.sleep(sleep_interval
)
206 raise PostProcessingError(f
'Unable to communicate with {self.PP_NAME} API: {e}')
209 class AudioConversionError(PostProcessingError
):