5 from ..networking
import Request
6 from ..networking
.exceptions
import HTTPError
, network_exceptions
16 class PostProcessorMetaClass(type):
18 def run_wrapper(func
):
19 @functools.wraps(func
)
20 def run(self
, info
, *args
, **kwargs
):
21 info_copy
= self
._copy
_infodict
(info
)
22 self
._hook
_progress
({'status': 'started'}, info_copy
)
23 ret
= func(self
, info
, *args
, **kwargs
)
26 self
._hook
_progress
({'status': 'finished'}, info_copy
)
30 def __new__(cls
, name
, bases
, attrs
):
32 attrs
['run'] = cls
.run_wrapper(attrs
['run'])
33 return type.__new
__(cls
, name
, bases
, attrs
)
36 class PostProcessor(metaclass
=PostProcessorMetaClass
):
37 """Post Processor class.
39 PostProcessor objects can be added to downloaders with their
40 add_post_processor() method. When the downloader has finished a
41 successful download, it will take its internal chain of PostProcessors
42 and start calling the run() method on each one of them, first with
43 an initial argument and then with the returned value of the previous
46 PostProcessor objects follow a "mutual registration" process similar
47 to InfoExtractor objects.
49 Optionally PostProcessor can use a list of additional command-line arguments
50 with self._configuration_args.
55 def __init__(self
, downloader
=None):
56 self
._progress
_hooks
= []
57 self
.add_progress_hook(self
.report_progress
)
58 self
.set_downloader(downloader
)
59 self
.PP_NAME
= self
.pp_key()
63 name
= cls
.__name
__[:-2]
64 return name
[6:] if name
[:6].lower() == 'ffmpeg' else name
66 def to_screen(self
, text
, prefix
=True, *args
, **kwargs
):
68 tag
= '[%s] ' % self
.PP_NAME
if prefix
else ''
69 return self
._downloader
.to_screen(f
'{tag}{text}', *args
, **kwargs
)
71 def report_warning(self
, text
, *args
, **kwargs
):
73 return self
._downloader
.report_warning(text
, *args
, **kwargs
)
75 def deprecation_warning(self
, msg
):
76 warn
= getattr(self
._downloader
, 'deprecation_warning', deprecation_warning
)
77 return warn(msg
, stacklevel
=1)
79 def deprecated_feature(self
, msg
):
81 return self
._downloader
.deprecated_feature(msg
)
82 return deprecation_warning(msg
, stacklevel
=1)
84 def report_error(self
, text
, *args
, **kwargs
):
85 self
.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
86 'raise "yt_dlp.utils.PostProcessingError" instead')
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 _delete_downloaded_files(self
, *files_to_delete
, **kwargs
):
96 return self
._downloader
._delete
_downloaded
_files
(*files_to_delete
, **kwargs
)
97 for filename
in set(filter(None, files_to_delete
)):
100 def get_param(self
, name
, default
=None, *args
, **kwargs
):
102 return self
._downloader
.params
.get(name
, default
, *args
, **kwargs
)
105 def set_downloader(self
, downloader
):
106 """Sets the downloader for this PP."""
107 self
._downloader
= downloader
108 for ph
in getattr(downloader
, '_postprocessor_hooks', []):
109 self
.add_progress_hook(ph
)
111 def _copy_infodict(self
, info_dict
):
112 return getattr(self
._downloader
, '_copy_infodict', dict)(info_dict
)
115 def _restrict_to(*, video
=True, audio
=True, images
=True, simulated
=True):
116 allowed
= {'video': video
, 'audio': audio
, 'images': images
}
119 @functools.wraps(func
)
120 def wrapper(self
, info
):
121 if not simulated
and (self
.get_param('simulate') or self
.get_param('skip_download')):
124 'video' if info
.get('vcodec') != 'none'
125 else 'audio' if info
.get('acodec') != 'none'
127 if allowed
[format_type
]:
128 return func(self
, info
)
130 self
.to_screen('Skipping %s' % format_type
)
135 def run(self
, information
):
136 """Run the PostProcessor.
138 The "information" argument is a dictionary like the ones
139 composed by InfoExtractors. The only difference is that this
140 one has an extra field called "filepath" that points to the
143 This method returns a tuple, the first element is a list of the files
144 that can be deleted, and the second of which is the updated
147 In addition, this method may raise a PostProcessingError
148 exception if post processing fails.
150 return [], information
# by default, keep file and do nothing
152 def try_utime(self
, path
, atime
, mtime
, errnote
='Cannot update utime of file'):
154 os
.utime(encodeFilename(path
), (atime
, mtime
))
156 self
.report_warning(errnote
)
158 def _configuration_args(self
, exe
, *args
, **kwargs
):
159 return _configuration_args(
160 self
.pp_key(), self
.get_param('postprocessor_args'), exe
, *args
, **kwargs
)
162 def _hook_progress(self
, status
, info_dict
):
163 if not self
._progress
_hooks
:
166 'info_dict': info_dict
,
167 'postprocessor': self
.pp_key(),
169 for ph
in self
._progress
_hooks
:
172 def add_progress_hook(self
, ph
):
173 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
174 self
._progress
_hooks
.append(ph
)
176 def report_progress(self
, s
):
177 s
['_default_template'] = '%(postprocessor)s %(status)s' % s
178 if not self
._downloader
:
181 progress_dict
= s
.copy()
182 progress_dict
.pop('info_dict')
183 progress_dict
= {'info': s
['info_dict'], 'progress': progress_dict
}
185 progress_template
= self
.get_param('progress_template', {})
186 tmpl
= progress_template
.get('postprocess')
188 self
._downloader
.to_screen(
189 self
._downloader
.evaluate_outtmpl(tmpl
, progress_dict
), quiet
=False)
191 self
._downloader
.to_console_title(self
._downloader
.evaluate_outtmpl(
192 progress_template
.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
195 def _retry_download(self
, err
, count
, retries
):
196 # While this is not an extractor, it behaves similar to one and
197 # so obey extractor_retries and "--retry-sleep extractor"
198 RetryManager
.report_retry(err
, count
, retries
, info
=self
.to_screen
, warn
=self
.report_warning
,
199 sleep_func
=self
.get_param('retry_sleep_functions', {}).get('extractor'))
201 def _download_json(self
, url
, *, expected_http_errors
=(404,)):
202 self
.write_debug(f
'{self.PP_NAME} query: {url}')
203 for retry
in RetryManager(self
.get_param('extractor_retries', 3), self
._retry
_download
):
205 rsp
= self
._downloader
.urlopen(Request(url
))
206 except network_exceptions
as e
:
207 if isinstance(e
, HTTPError
) and e
.status
in expected_http_errors
:
209 retry
.error
= PostProcessingError(f
'Unable to communicate with {self.PP_NAME} API: {e}')
211 return json
.loads(rsp
.read().decode(rsp
.headers
.get_param('charset') or 'utf-8'))
214 class AudioConversionError(PostProcessingError
): # Deprecated