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
):
73 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 self
.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
87 'raise "yt_dlp.utils.PostProcessingError" instead')
89 return self
._downloader
.report_error(text
, *args
, **kwargs
)
91 def write_debug(self
, text
, *args
, **kwargs
):
93 return self
._downloader
.write_debug(text
, *args
, **kwargs
)
95 def get_param(self
, name
, default
=None, *args
, **kwargs
):
97 return self
._downloader
.params
.get(name
, default
, *args
, **kwargs
)
100 def set_downloader(self
, downloader
):
101 """Sets the downloader for this PP."""
102 self
._downloader
= downloader
103 for ph
in getattr(downloader
, '_postprocessor_hooks', []):
104 self
.add_progress_hook(ph
)
106 def _copy_infodict(self
, info_dict
):
107 return getattr(self
._downloader
, '_copy_infodict', dict)(info_dict
)
110 def _restrict_to(*, video
=True, audio
=True, images
=True, simulated
=True):
111 allowed
= {'video': video, 'audio': audio, 'images': images}
114 @functools.wraps(func
)
115 def wrapper(self
, info
):
116 if not simulated
and (self
.get_param('simulate') or self
.get_param('skip_download')):
119 'video' if info
.get('vcodec') != 'none'
120 else 'audio' if info
.get('acodec') != 'none'
122 if allowed
[format_type
]:
123 return func(self
, info
)
125 self
.to_screen('Skipping %s' % format_type
)
130 def run(self
, information
):
131 """Run the PostProcessor.
133 The "information" argument is a dictionary like the ones
134 composed by InfoExtractors. The only difference is that this
135 one has an extra field called "filepath" that points to the
138 This method returns a tuple, the first element is a list of the files
139 that can be deleted, and the second of which is the updated
142 In addition, this method may raise a PostProcessingError
143 exception if post processing fails.
145 return [], information
# by default, keep file and do nothing
147 def try_utime(self
, path
, atime
, mtime
, errnote
='Cannot update utime of file'):
149 os
.utime(encodeFilename(path
), (atime
, mtime
))
151 self
.report_warning(errnote
)
153 def _configuration_args(self
, exe
, *args
, **kwargs
):
154 return _configuration_args(
155 self
.pp_key(), self
.get_param('postprocessor_args'), exe
, *args
, **kwargs
)
157 def _hook_progress(self
, status
, info_dict
):
158 if not self
._progress
_hooks
:
161 'info_dict': info_dict
,
162 'postprocessor': self
.pp_key(),
164 for ph
in self
._progress
_hooks
:
167 def add_progress_hook(self
, ph
):
168 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
169 self
._progress
_hooks
.append(ph
)
171 def report_progress(self
, s
):
172 s
['_default_template'] = '%(postprocessor)s %(status)s' % s
174 progress_dict
= s
.copy()
175 progress_dict
.pop('info_dict')
176 progress_dict
= {'info': s['info_dict'], 'progress': progress_dict}
178 progress_template
= self
.get_param('progress_template', {})
179 tmpl
= progress_template
.get('postprocess')
181 self
._downloader
.to_stdout(self
._downloader
.evaluate_outtmpl(tmpl
, progress_dict
))
183 self
._downloader
.to_console_title(self
._downloader
.evaluate_outtmpl(
184 progress_template
.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
187 def _download_json(self
, url
, *, expected_http_errors
=(404,)):
188 # While this is not an extractor, it behaves similar to one and
189 # so obey extractor_retries and sleep_interval_requests
190 max_retries
= self
.get_param('extractor_retries', 3)
191 sleep_interval
= self
.get_param('sleep_interval_requests') or 0
193 self
.write_debug(f
'{self.PP_NAME} query: {url}')
194 for retries
in itertools
.count():
196 rsp
= self
._downloader
.urlopen(sanitized_Request(url
))
197 return json
.loads(rsp
.read().decode(rsp
.info().get_param('charset') or 'utf-8'))
198 except network_exceptions
as e
:
199 if isinstance(e
, urllib
.error
.HTTPError
) and e
.code
in expected_http_errors
:
201 if retries
< max_retries
:
202 self
.report_warning(f
'{e}. Retrying...')
203 if sleep_interval
> 0:
204 self
.to_screen(f
'Sleeping {sleep_interval} seconds ...')
205 time
.sleep(sleep_interval
)
207 raise PostProcessingError(f
'Unable to communicate with {self.PP_NAME} API: {e}')
210 class AudioConversionError(PostProcessingError
):