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