]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/common.py
Standardize retry mechanism (#1649)
[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,
dd29eb7f 10 encodeFilename,
a3f2445e 11 network_exceptions,
a3f2445e 12 sanitized_Request,
ee8dd27a 13 write_string,
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
ee8dd27a 76 def deprecation_warning(self, text):
77 if self._downloader:
78 return self._downloader.deprecation_warning(text)
79 write_string(f'DeprecationWarning: {text}')
80
f446cc66 81 def report_error(self, text, *args, **kwargs):
3d3bb168 82 self.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
83 'raise "yt_dlp.utils.PostProcessingError" instead')
f446cc66 84 if self._downloader:
85 return self._downloader.report_error(text, *args, **kwargs)
86
0760b0a7 87 def write_debug(self, text, *args, **kwargs):
88 if self._downloader:
89 return self._downloader.write_debug(text, *args, **kwargs)
f446cc66 90
43d7f5a5 91 def _delete_downloaded_files(self, *files_to_delete, **kwargs):
0f06bcd7 92 if self._downloader:
93 return self._downloader._delete_downloaded_files(*files_to_delete, **kwargs)
94 for filename in set(filter(None, files_to_delete)):
95 os.remove(filename)
43d7f5a5 96
f446cc66 97 def get_param(self, name, default=None, *args, **kwargs):
98 if self._downloader:
99 return self._downloader.params.get(name, default, *args, **kwargs)
100 return default
496c1923
PH
101
102 def set_downloader(self, downloader):
103 """Sets the downloader for this PP."""
104 self._downloader = downloader
aa9a92fd 105 for ph in getattr(downloader, '_postprocessor_hooks', []):
819e0531 106 self.add_progress_hook(ph)
496c1923 107
03b4de72 108 def _copy_infodict(self, info_dict):
109 return getattr(self._downloader, '_copy_infodict', dict)(info_dict)
110
8326b00a 111 @staticmethod
ed66a17e 112 def _restrict_to(*, video=True, audio=True, images=True, simulated=True):
8326b00a 113 allowed = {'video': video, 'audio': audio, 'images': images}
114
115 def decorator(func):
116 @functools.wraps(func)
117 def wrapper(self, info):
ed66a17e 118 if not simulated and (self.get_param('simulate') or self.get_param('skip_download')):
119 return [], info
8326b00a 120 format_type = (
7e87e27c 121 'video' if info.get('vcodec') != 'none'
122 else 'audio' if info.get('acodec') != 'none'
8326b00a 123 else 'images')
124 if allowed[format_type]:
4d85fbbd 125 return func(self, info)
8326b00a 126 else:
127 self.to_screen('Skipping %s' % format_type)
128 return [], info
129 return wrapper
130 return decorator
131
496c1923
PH
132 def run(self, information):
133 """Run the PostProcessor.
134
135 The "information" argument is a dictionary like the ones
136 composed by InfoExtractors. The only difference is that this
137 one has an extra field called "filepath" that points to the
138 downloaded file.
139
592e97e8
JMF
140 This method returns a tuple, the first element is a list of the files
141 that can be deleted, and the second of which is the updated
142 information.
496c1923
PH
143
144 In addition, this method may raise a PostProcessingError
145 exception if post processing fails.
146 """
592e97e8 147 return [], information # by default, keep file and do nothing
496c1923 148
dd29eb7f
S
149 def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
150 try:
151 os.utime(encodeFilename(path), (atime, mtime))
152 except Exception:
f446cc66 153 self.report_warning(errnote)
dd29eb7f 154
330690a2 155 def _configuration_args(self, exe, *args, **kwargs):
156 return _configuration_args(
157 self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
e35b23f5 158
819e0531 159 def _hook_progress(self, status, info_dict):
160 if not self._progress_hooks:
161 return
819e0531 162 status.update({
03b4de72 163 'info_dict': info_dict,
819e0531 164 'postprocessor': self.pp_key(),
165 })
166 for ph in self._progress_hooks:
167 ph(status)
168
169 def add_progress_hook(self, ph):
170 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
171 self._progress_hooks.append(ph)
172
173 def report_progress(self, s):
174 s['_default_template'] = '%(postprocessor)s %(status)s' % s
8a82af35 175 if not self._downloader:
176 return
819e0531 177
178 progress_dict = s.copy()
179 progress_dict.pop('info_dict')
180 progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
181
182 progress_template = self.get_param('progress_template', {})
183 tmpl = progress_template.get('postprocess')
184 if tmpl:
8a82af35 185 self._downloader.to_screen(
186 self._downloader.evaluate_outtmpl(tmpl, progress_dict), skip_eol=True, quiet=False)
819e0531 187
188 self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
189 progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
190 progress_dict))
191
be5c1ae8 192 def _retry_download(self, err, count, retries):
a3f2445e 193 # While this is not an extractor, it behaves similar to one and
194 # so obey extractor_retries and sleep_interval_requests
be5c1ae8 195 RetryManager.report_retry(err, count, retries, info=self.to_screen, warn=self.report_warning,
196 sleep_func=self.get_param('sleep_interval_requests'))
a3f2445e 197
be5c1ae8 198 def _download_json(self, url, *, expected_http_errors=(404,)):
a3f2445e 199 self.write_debug(f'{self.PP_NAME} query: {url}')
be5c1ae8 200 for retry in RetryManager(self.get_param('extractor_retries', 3), self._retry_download):
a3f2445e 201 try:
202 rsp = self._downloader.urlopen(sanitized_Request(url))
a3f2445e 203 except network_exceptions as e:
204 if isinstance(e, urllib.error.HTTPError) and e.code in expected_http_errors:
205 return None
be5c1ae8 206 retry.error = PostProcessingError(f'Unable to communicate with {self.PP_NAME} API: {e}')
207 continue
208 return json.loads(rsp.read().decode(rsp.info().get_param('charset') or 'utf-8'))
a3f2445e 209
496c1923 210
35faefee 211class AudioConversionError(PostProcessingError): # Deprecated
496c1923 212 pass