]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/common.py
[utils] Add `deprecation_warning`
[yt-dlp.git] / yt_dlp / postprocessor / common.py
CommitLineData
8326b00a 1import functools
a3f2445e 2import json
dd29eb7f 3import os
a3f2445e 4import urllib.error
dd29eb7f
S
5
6from ..utils import (
f8271158 7 PostProcessingError,
be5c1ae8 8 RetryManager,
330690a2 9 _configuration_args,
da4db748 10 deprecation_warning,
dd29eb7f 11 encodeFilename,
a3f2445e 12 network_exceptions,
a3f2445e 13 sanitized_Request,
dd29eb7f 14)
496c1923
PH
15
16
819e0531 17class PostProcessorMetaClass(type):
18 @staticmethod
19 def run_wrapper(func):
20 @functools.wraps(func)
21 def run(self, info, *args, **kwargs):
adbc4ec4 22 info_copy = self._copy_infodict(info)
03b4de72 23 self._hook_progress({'status': 'started'}, info_copy)
819e0531 24 ret = func(self, info, *args, **kwargs)
25 if ret is not None:
26 _, info = ret
03b4de72 27 self._hook_progress({'status': 'finished'}, info_copy)
819e0531 28 return ret
29 return run
30
31 def __new__(cls, name, bases, attrs):
32 if 'run' in attrs:
33 attrs['run'] = cls.run_wrapper(attrs['run'])
34 return type.__new__(cls, name, bases, attrs)
35
36
37class PostProcessor(metaclass=PostProcessorMetaClass):
496c1923
PH
38 """Post Processor class.
39
40 PostProcessor objects can be added to downloaders with their
41 add_post_processor() method. When the downloader has finished a
42 successful download, it will take its internal chain of PostProcessors
43 and start calling the run() method on each one of them, first with
44 an initial argument and then with the returned value of the previous
45 PostProcessor.
46
496c1923 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]
a3f2445e 65 return name[6:] if name[:6].lower() == 'ffmpeg' else name
1b77b347 66
fbced734 67 def to_screen(self, text, prefix=True, *args, **kwargs):
f446cc66 68 if self._downloader:
19a03940 69 tag = '[%s] ' % self.PP_NAME if prefix else ''
86e5f3ed 70 return self._downloader.to_screen(f'{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
da4db748 76 def deprecation_warning(self, msg):
77 warn = getattr(self._downloader, 'deprecation_warning', deprecation_warning)
78 return warn(msg, stacklevel=1)
79
80 def deprecated_feature(self, msg):
ee8dd27a 81 if self._downloader:
da4db748 82 return self._downloader.deprecated_feature(msg)
83 return deprecation_warning(msg, stacklevel=1)
ee8dd27a 84
f446cc66 85 def report_error(self, text, *args, **kwargs):
3d3bb168 86 self.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
87 'raise "yt_dlp.utils.PostProcessingError" instead')
f446cc66 88 if self._downloader:
89 return self._downloader.report_error(text, *args, **kwargs)
90
0760b0a7 91 def write_debug(self, text, *args, **kwargs):
92 if self._downloader:
93 return self._downloader.write_debug(text, *args, **kwargs)
f446cc66 94
43d7f5a5 95 def _delete_downloaded_files(self, *files_to_delete, **kwargs):
0f06bcd7 96 if self._downloader:
97 return self._downloader._delete_downloaded_files(*files_to_delete, **kwargs)
98 for filename in set(filter(None, files_to_delete)):
99 os.remove(filename)
43d7f5a5 100
f446cc66 101 def get_param(self, name, default=None, *args, **kwargs):
102 if self._downloader:
103 return self._downloader.params.get(name, default, *args, **kwargs)
104 return default
496c1923
PH
105
106 def set_downloader(self, downloader):
107 """Sets the downloader for this PP."""
108 self._downloader = downloader
aa9a92fd 109 for ph in getattr(downloader, '_postprocessor_hooks', []):
819e0531 110 self.add_progress_hook(ph)
496c1923 111
03b4de72 112 def _copy_infodict(self, info_dict):
113 return getattr(self._downloader, '_copy_infodict', dict)(info_dict)
114
8326b00a 115 @staticmethod
ed66a17e 116 def _restrict_to(*, video=True, audio=True, images=True, simulated=True):
8326b00a 117 allowed = {'video': video, 'audio': audio, 'images': images}
118
119 def decorator(func):
120 @functools.wraps(func)
121 def wrapper(self, info):
ed66a17e 122 if not simulated and (self.get_param('simulate') or self.get_param('skip_download')):
123 return [], info
8326b00a 124 format_type = (
7e87e27c 125 'video' if info.get('vcodec') != 'none'
126 else 'audio' if info.get('acodec') != 'none'
8326b00a 127 else 'images')
128 if allowed[format_type]:
4d85fbbd 129 return func(self, info)
8326b00a 130 else:
131 self.to_screen('Skipping %s' % format_type)
132 return [], info
133 return wrapper
134 return decorator
135
496c1923
PH
136 def run(self, information):
137 """Run the PostProcessor.
138
139 The "information" argument is a dictionary like the ones
140 composed by InfoExtractors. The only difference is that this
141 one has an extra field called "filepath" that points to the
142 downloaded file.
143
592e97e8
JMF
144 This method returns a tuple, the first element is a list of the files
145 that can be deleted, and the second of which is the updated
146 information.
496c1923
PH
147
148 In addition, this method may raise a PostProcessingError
149 exception if post processing fails.
150 """
592e97e8 151 return [], information # by default, keep file and do nothing
496c1923 152
dd29eb7f
S
153 def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
154 try:
155 os.utime(encodeFilename(path), (atime, mtime))
156 except Exception:
f446cc66 157 self.report_warning(errnote)
dd29eb7f 158
330690a2 159 def _configuration_args(self, exe, *args, **kwargs):
160 return _configuration_args(
161 self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
e35b23f5 162
819e0531 163 def _hook_progress(self, status, info_dict):
164 if not self._progress_hooks:
165 return
819e0531 166 status.update({
03b4de72 167 'info_dict': info_dict,
819e0531 168 'postprocessor': self.pp_key(),
169 })
170 for ph in self._progress_hooks:
171 ph(status)
172
173 def add_progress_hook(self, ph):
174 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
175 self._progress_hooks.append(ph)
176
177 def report_progress(self, s):
178 s['_default_template'] = '%(postprocessor)s %(status)s' % s
8a82af35 179 if not self._downloader:
180 return
819e0531 181
182 progress_dict = s.copy()
183 progress_dict.pop('info_dict')
184 progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
185
186 progress_template = self.get_param('progress_template', {})
187 tmpl = progress_template.get('postprocess')
188 if tmpl:
8a82af35 189 self._downloader.to_screen(
190 self._downloader.evaluate_outtmpl(tmpl, progress_dict), skip_eol=True, quiet=False)
819e0531 191
192 self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
193 progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
194 progress_dict))
195
be5c1ae8 196 def _retry_download(self, err, count, retries):
a3f2445e 197 # While this is not an extractor, it behaves similar to one and
198 # so obey extractor_retries and sleep_interval_requests
be5c1ae8 199 RetryManager.report_retry(err, count, retries, info=self.to_screen, warn=self.report_warning,
200 sleep_func=self.get_param('sleep_interval_requests'))
a3f2445e 201
be5c1ae8 202 def _download_json(self, url, *, expected_http_errors=(404,)):
a3f2445e 203 self.write_debug(f'{self.PP_NAME} query: {url}')
be5c1ae8 204 for retry in RetryManager(self.get_param('extractor_retries', 3), self._retry_download):
a3f2445e 205 try:
206 rsp = self._downloader.urlopen(sanitized_Request(url))
a3f2445e 207 except network_exceptions as e:
208 if isinstance(e, urllib.error.HTTPError) and e.code in expected_http_errors:
209 return None
be5c1ae8 210 retry.error = PostProcessingError(f'Unable to communicate with {self.PP_NAME} API: {e}')
211 continue
212 return json.loads(rsp.read().decode(rsp.info().get_param('charset') or 'utf-8'))
a3f2445e 213
496c1923 214
35faefee 215class AudioConversionError(PostProcessingError): # Deprecated
496c1923 216 pass