]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/common.py
[cleanup] Misc fixes
[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
496c1923 48 PostProcessor objects follow a "mutual registration" process similar
e35b23f5
S
49 to InfoExtractor objects.
50
51 Optionally PostProcessor can use a list of additional command-line arguments
52 with self._configuration_args.
496c1923
PH
53 """
54
55 _downloader = None
56
aa5d9a79 57 def __init__(self, downloader=None):
819e0531 58 self._progress_hooks = []
59 self.add_progress_hook(self.report_progress)
60 self.set_downloader(downloader)
43820c03 61 self.PP_NAME = self.pp_key()
62
63 @classmethod
64 def pp_key(cls):
65 name = cls.__name__[:-2]
a3f2445e 66 return name[6:] if name[:6].lower() == 'ffmpeg' else name
1b77b347 67
fbced734 68 def to_screen(self, text, prefix=True, *args, **kwargs):
f446cc66 69 if self._downloader:
19a03940 70 tag = '[%s] ' % self.PP_NAME if prefix else ''
86e5f3ed 71 return self._downloader.to_screen(f'{tag}{text}', *args, **kwargs)
f446cc66 72
73 def report_warning(self, text, *args, **kwargs):
74 if self._downloader:
75 return self._downloader.report_warning(text, *args, **kwargs)
76
ee8dd27a 77 def deprecation_warning(self, text):
78 if self._downloader:
79 return self._downloader.deprecation_warning(text)
80 write_string(f'DeprecationWarning: {text}')
81
f446cc66 82 def report_error(self, text, *args, **kwargs):
3d3bb168 83 self.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
84 'raise "yt_dlp.utils.PostProcessingError" instead')
f446cc66 85 if self._downloader:
86 return self._downloader.report_error(text, *args, **kwargs)
87
0760b0a7 88 def write_debug(self, text, *args, **kwargs):
89 if self._downloader:
90 return self._downloader.write_debug(text, *args, **kwargs)
f446cc66 91
43d7f5a5 92 def _delete_downloaded_files(self, *files_to_delete, **kwargs):
0f06bcd7 93 if self._downloader:
94 return self._downloader._delete_downloaded_files(*files_to_delete, **kwargs)
95 for filename in set(filter(None, files_to_delete)):
96 os.remove(filename)
43d7f5a5 97
f446cc66 98 def get_param(self, name, default=None, *args, **kwargs):
99 if self._downloader:
100 return self._downloader.params.get(name, default, *args, **kwargs)
101 return default
496c1923
PH
102
103 def set_downloader(self, downloader):
104 """Sets the downloader for this PP."""
105 self._downloader = downloader
aa9a92fd 106 for ph in getattr(downloader, '_postprocessor_hooks', []):
819e0531 107 self.add_progress_hook(ph)
496c1923 108
03b4de72 109 def _copy_infodict(self, info_dict):
110 return getattr(self._downloader, '_copy_infodict', dict)(info_dict)
111
8326b00a 112 @staticmethod
ed66a17e 113 def _restrict_to(*, video=True, audio=True, images=True, simulated=True):
8326b00a 114 allowed = {'video': video, 'audio': audio, 'images': images}
115
116 def decorator(func):
117 @functools.wraps(func)
118 def wrapper(self, info):
ed66a17e 119 if not simulated and (self.get_param('simulate') or self.get_param('skip_download')):
120 return [], info
8326b00a 121 format_type = (
7e87e27c 122 'video' if info.get('vcodec') != 'none'
123 else 'audio' if info.get('acodec') != 'none'
8326b00a 124 else 'images')
125 if allowed[format_type]:
4d85fbbd 126 return func(self, info)
8326b00a 127 else:
128 self.to_screen('Skipping %s' % format_type)
129 return [], info
130 return wrapper
131 return decorator
132
496c1923
PH
133 def run(self, information):
134 """Run the PostProcessor.
135
136 The "information" argument is a dictionary like the ones
137 composed by InfoExtractors. The only difference is that this
138 one has an extra field called "filepath" that points to the
139 downloaded file.
140
592e97e8
JMF
141 This method returns a tuple, the first element is a list of the files
142 that can be deleted, and the second of which is the updated
143 information.
496c1923
PH
144
145 In addition, this method may raise a PostProcessingError
146 exception if post processing fails.
147 """
592e97e8 148 return [], information # by default, keep file and do nothing
496c1923 149
dd29eb7f
S
150 def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
151 try:
152 os.utime(encodeFilename(path), (atime, mtime))
153 except Exception:
f446cc66 154 self.report_warning(errnote)
dd29eb7f 155
330690a2 156 def _configuration_args(self, exe, *args, **kwargs):
157 return _configuration_args(
158 self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
e35b23f5 159
819e0531 160 def _hook_progress(self, status, info_dict):
161 if not self._progress_hooks:
162 return
819e0531 163 status.update({
03b4de72 164 'info_dict': info_dict,
819e0531 165 'postprocessor': self.pp_key(),
166 })
167 for ph in self._progress_hooks:
168 ph(status)
169
170 def add_progress_hook(self, ph):
171 # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
172 self._progress_hooks.append(ph)
173
174 def report_progress(self, s):
175 s['_default_template'] = '%(postprocessor)s %(status)s' % s
8a82af35 176 if not self._downloader:
177 return
819e0531 178
179 progress_dict = s.copy()
180 progress_dict.pop('info_dict')
181 progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
182
183 progress_template = self.get_param('progress_template', {})
184 tmpl = progress_template.get('postprocess')
185 if tmpl:
8a82af35 186 self._downloader.to_screen(
187 self._downloader.evaluate_outtmpl(tmpl, progress_dict), skip_eol=True, quiet=False)
819e0531 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 215
35faefee 216class AudioConversionError(PostProcessingError): # Deprecated
496c1923 217 pass