]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/common.py
[ffmpeg] Fix features detection
[yt-dlp.git] / yt_dlp / postprocessor / common.py
CommitLineData
8326b00a 1import functools
a3f2445e 2import itertools
3import json
dd29eb7f 4import os
a3f2445e 5import time
6import urllib.error
dd29eb7f
S
7
8from ..utils import (
f8271158 9 PostProcessingError,
330690a2 10 _configuration_args,
dd29eb7f 11 encodeFilename,
a3f2445e 12 network_exceptions,
a3f2445e 13 sanitized_Request,
ee8dd27a 14 write_string,
dd29eb7f 15)
496c1923
PH
16
17
819e0531 18class PostProcessorMetaClass(type):
19 @staticmethod
20 def run_wrapper(func):
21 @functools.wraps(func)
22 def run(self, info, *args, **kwargs):
adbc4ec4 23 info_copy = self._copy_infodict(info)
03b4de72 24 self._hook_progress({'status': 'started'}, info_copy)
819e0531 25 ret = func(self, info, *args, **kwargs)
26 if ret is not None:
27 _, info = ret
03b4de72 28 self._hook_progress({'status': 'finished'}, info_copy)
819e0531 29 return ret
30 return run
31
32 def __new__(cls, name, bases, attrs):
33 if 'run' in attrs:
34 attrs['run'] = cls.run_wrapper(attrs['run'])
35 return type.__new__(cls, name, bases, attrs)
36
37
38class PostProcessor(metaclass=PostProcessorMetaClass):
496c1923
PH
39 """Post Processor class.
40
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
46 PostProcessor.
47
48 The chain will be stopped if one of them ever returns None or the end
49 of the chain is reached.
50
51 PostProcessor objects follow a "mutual registration" process similar
e35b23f5
S
52 to InfoExtractor objects.
53
54 Optionally PostProcessor can use a list of additional command-line arguments
55 with self._configuration_args.
496c1923
PH
56 """
57
58 _downloader = None
59
aa5d9a79 60 def __init__(self, downloader=None):
819e0531 61 self._progress_hooks = []
62 self.add_progress_hook(self.report_progress)
63 self.set_downloader(downloader)
43820c03 64 self.PP_NAME = self.pp_key()
65
66 @classmethod
67 def pp_key(cls):
68 name = cls.__name__[:-2]
a3f2445e 69 return name[6:] if name[:6].lower() == 'ffmpeg' else name
1b77b347 70
fbced734 71 def to_screen(self, text, prefix=True, *args, **kwargs):
f446cc66 72 if self._downloader:
19a03940 73 tag = '[%s] ' % self.PP_NAME if prefix else ''
86e5f3ed 74 return self._downloader.to_screen(f'{tag}{text}', *args, **kwargs)
f446cc66 75
76 def report_warning(self, text, *args, **kwargs):
77 if self._downloader:
78 return self._downloader.report_warning(text, *args, **kwargs)
79
ee8dd27a 80 def deprecation_warning(self, text):
81 if self._downloader:
82 return self._downloader.deprecation_warning(text)
83 write_string(f'DeprecationWarning: {text}')
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
95 def get_param(self, name, default=None, *args, **kwargs):
96 if self._downloader:
97 return self._downloader.params.get(name, default, *args, **kwargs)
98 return default
496c1923
PH
99
100 def set_downloader(self, downloader):
101 """Sets the downloader for this PP."""
102 self._downloader = downloader
aa9a92fd 103 for ph in getattr(downloader, '_postprocessor_hooks', []):
819e0531 104 self.add_progress_hook(ph)
496c1923 105
03b4de72 106 def _copy_infodict(self, info_dict):
107 return getattr(self._downloader, '_copy_infodict', dict)(info_dict)
108
8326b00a 109 @staticmethod
ed66a17e 110 def _restrict_to(*, video=True, audio=True, images=True, simulated=True):
8326b00a 111 allowed = {'video': video, 'audio': audio, 'images': images}
112
113 def decorator(func):
114 @functools.wraps(func)
115 def wrapper(self, info):
ed66a17e 116 if not simulated and (self.get_param('simulate') or self.get_param('skip_download')):
117 return [], info
8326b00a 118 format_type = (
7e87e27c 119 'video' if info.get('vcodec') != 'none'
120 else 'audio' if info.get('acodec') != 'none'
8326b00a 121 else 'images')
122 if allowed[format_type]:
4d85fbbd 123 return func(self, info)
8326b00a 124 else:
125 self.to_screen('Skipping %s' % format_type)
126 return [], info
127 return wrapper
128 return decorator
129
496c1923
PH
130 def run(self, information):
131 """Run the PostProcessor.
132
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
136 downloaded file.
137
592e97e8
JMF
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
140 information.
496c1923
PH
141
142 In addition, this method may raise a PostProcessingError
143 exception if post processing fails.
144 """
592e97e8 145 return [], information # by default, keep file and do nothing
496c1923 146
dd29eb7f
S
147 def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
148 try:
149 os.utime(encodeFilename(path), (atime, mtime))
150 except Exception:
f446cc66 151 self.report_warning(errnote)
dd29eb7f 152
330690a2 153 def _configuration_args(self, exe, *args, **kwargs):
154 return _configuration_args(
155 self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
e35b23f5 156
819e0531 157 def _hook_progress(self, status, info_dict):
158 if not self._progress_hooks:
159 return
819e0531 160 status.update({
03b4de72 161 'info_dict': info_dict,
819e0531 162 'postprocessor': self.pp_key(),
163 })
164 for ph in self._progress_hooks:
165 ph(status)
166
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)
170
171 def report_progress(self, s):
172 s['_default_template'] = '%(postprocessor)s %(status)s' % s
173
174 progress_dict = s.copy()
175 progress_dict.pop('info_dict')
176 progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
177
178 progress_template = self.get_param('progress_template', {})
179 tmpl = progress_template.get('postprocess')
180 if tmpl:
181 self._downloader.to_stdout(self._downloader.evaluate_outtmpl(tmpl, progress_dict))
182
183 self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
184 progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
185 progress_dict))
186
a3f2445e 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
192
193 self.write_debug(f'{self.PP_NAME} query: {url}')
194 for retries in itertools.count():
195 try:
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:
200 return None
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)
206 continue
207 raise PostProcessingError(f'Unable to communicate with {self.PP_NAME} API: {e}')
208
496c1923
PH
209
210class AudioConversionError(PostProcessingError):
211 pass