]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/common.py
[cleanup] Minor fixes (See desc)
[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
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
179
180 progress_dict = s.copy()
181 progress_dict.pop('info_dict')
182 progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
183
184 progress_template = self.get_param('progress_template', {})
185 tmpl = progress_template.get('postprocess')
186 if tmpl:
187 self._downloader.to_stdout(self._downloader.evaluate_outtmpl(tmpl, progress_dict))
188
189 self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
190 progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
191 progress_dict))
192
a3f2445e 193 def _download_json(self, url, *, expected_http_errors=(404,)):
194 # While this is not an extractor, it behaves similar to one and
195 # so obey extractor_retries and sleep_interval_requests
196 max_retries = self.get_param('extractor_retries', 3)
197 sleep_interval = self.get_param('sleep_interval_requests') or 0
198
199 self.write_debug(f'{self.PP_NAME} query: {url}')
200 for retries in itertools.count():
201 try:
202 rsp = self._downloader.urlopen(sanitized_Request(url))
203 return json.loads(rsp.read().decode(rsp.info().get_param('charset') or 'utf-8'))
204 except network_exceptions as e:
205 if isinstance(e, urllib.error.HTTPError) and e.code in expected_http_errors:
206 return None
207 if retries < max_retries:
208 self.report_warning(f'{e}. Retrying...')
209 if sleep_interval > 0:
210 self.to_screen(f'Sleeping {sleep_interval} seconds ...')
211 time.sleep(sleep_interval)
212 continue
213 raise PostProcessingError(f'Unable to communicate with {self.PP_NAME} API: {e}')
214
496c1923
PH
215
216class AudioConversionError(PostProcessingError):
217 pass