common.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import functools
  2. import json
  3. import os
  4. from ..networking import Request
  5. from ..networking.exceptions import HTTPError, network_exceptions
  6. from ..utils import (
  7. PostProcessingError,
  8. RetryManager,
  9. _configuration_args,
  10. deprecation_warning,
  11. )
  12. class PostProcessorMetaClass(type):
  13. @staticmethod
  14. def run_wrapper(func):
  15. @functools.wraps(func)
  16. def run(self, info, *args, **kwargs):
  17. info_copy = self._copy_infodict(info)
  18. self._hook_progress({'status': 'started'}, info_copy)
  19. ret = func(self, info, *args, **kwargs)
  20. if ret is not None:
  21. _, info = ret
  22. self._hook_progress({'status': 'finished'}, info_copy)
  23. return ret
  24. return run
  25. def __new__(cls, name, bases, attrs):
  26. if 'run' in attrs:
  27. attrs['run'] = cls.run_wrapper(attrs['run'])
  28. return type.__new__(cls, name, bases, attrs)
  29. class PostProcessor(metaclass=PostProcessorMetaClass):
  30. """Post Processor class.
  31. PostProcessor objects can be added to downloaders with their
  32. add_post_processor() method. When the downloader has finished a
  33. successful download, it will take its internal chain of PostProcessors
  34. and start calling the run() method on each one of them, first with
  35. an initial argument and then with the returned value of the previous
  36. PostProcessor.
  37. PostProcessor objects follow a "mutual registration" process similar
  38. to InfoExtractor objects.
  39. Optionally PostProcessor can use a list of additional command-line arguments
  40. with self._configuration_args.
  41. """
  42. _downloader = None
  43. def __init__(self, downloader=None):
  44. self._progress_hooks = []
  45. self.add_progress_hook(self.report_progress)
  46. self.set_downloader(downloader)
  47. self.PP_NAME = self.pp_key()
  48. @classmethod
  49. def pp_key(cls):
  50. name = cls.__name__[:-2]
  51. return name[6:] if name[:6].lower() == 'ffmpeg' else name
  52. def to_screen(self, text, prefix=True, *args, **kwargs):
  53. if self._downloader:
  54. tag = f'[{self.PP_NAME}] ' if prefix else ''
  55. return self._downloader.to_screen(f'{tag}{text}', *args, **kwargs)
  56. def report_warning(self, text, *args, **kwargs):
  57. if self._downloader:
  58. return self._downloader.report_warning(text, *args, **kwargs)
  59. def deprecation_warning(self, msg):
  60. warn = getattr(self._downloader, 'deprecation_warning', deprecation_warning)
  61. return warn(msg, stacklevel=1)
  62. def deprecated_feature(self, msg):
  63. if self._downloader:
  64. return self._downloader.deprecated_feature(msg)
  65. return deprecation_warning(msg, stacklevel=1)
  66. def report_error(self, text, *args, **kwargs):
  67. self.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
  68. 'raise "yt_dlp.utils.PostProcessingError" instead')
  69. if self._downloader:
  70. return self._downloader.report_error(text, *args, **kwargs)
  71. def write_debug(self, text, *args, **kwargs):
  72. if self._downloader:
  73. return self._downloader.write_debug(text, *args, **kwargs)
  74. def _delete_downloaded_files(self, *files_to_delete, **kwargs):
  75. if self._downloader:
  76. return self._downloader._delete_downloaded_files(*files_to_delete, **kwargs)
  77. for filename in set(filter(None, files_to_delete)):
  78. os.remove(filename)
  79. def get_param(self, name, default=None, *args, **kwargs):
  80. if self._downloader:
  81. return self._downloader.params.get(name, default, *args, **kwargs)
  82. return default
  83. def set_downloader(self, downloader):
  84. """Sets the downloader for this PP."""
  85. self._downloader = downloader
  86. for ph in getattr(downloader, '_postprocessor_hooks', []):
  87. self.add_progress_hook(ph)
  88. def _copy_infodict(self, info_dict):
  89. return getattr(self._downloader, '_copy_infodict', dict)(info_dict)
  90. @staticmethod
  91. def _restrict_to(*, video=True, audio=True, images=True, simulated=True):
  92. allowed = {'video': video, 'audio': audio, 'images': images}
  93. def decorator(func):
  94. @functools.wraps(func)
  95. def wrapper(self, info):
  96. if not simulated and (self.get_param('simulate') or self.get_param('skip_download')):
  97. return [], info
  98. format_type = (
  99. 'video' if info.get('vcodec') != 'none'
  100. else 'audio' if info.get('acodec') != 'none'
  101. else 'images')
  102. if allowed[format_type]:
  103. return func(self, info)
  104. else:
  105. self.to_screen(f'Skipping {format_type}')
  106. return [], info
  107. return wrapper
  108. return decorator
  109. def run(self, information):
  110. """Run the PostProcessor.
  111. The "information" argument is a dictionary like the ones
  112. composed by InfoExtractors. The only difference is that this
  113. one has an extra field called "filepath" that points to the
  114. downloaded file.
  115. This method returns a tuple, the first element is a list of the files
  116. that can be deleted, and the second of which is the updated
  117. information.
  118. In addition, this method may raise a PostProcessingError
  119. exception if post processing fails.
  120. """
  121. return [], information # by default, keep file and do nothing
  122. def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
  123. try:
  124. os.utime(path, (atime, mtime))
  125. except Exception:
  126. self.report_warning(errnote)
  127. def _configuration_args(self, exe, *args, **kwargs):
  128. return _configuration_args(
  129. self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
  130. def _hook_progress(self, status, info_dict):
  131. if not self._progress_hooks:
  132. return
  133. status.update({
  134. 'info_dict': info_dict,
  135. 'postprocessor': self.pp_key(),
  136. })
  137. for ph in self._progress_hooks:
  138. ph(status)
  139. def add_progress_hook(self, ph):
  140. # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
  141. self._progress_hooks.append(ph)
  142. def report_progress(self, s):
  143. s['_default_template'] = '%(postprocessor)s %(status)s' % s # noqa: UP031
  144. if not self._downloader:
  145. return
  146. progress_dict = s.copy()
  147. progress_dict.pop('info_dict')
  148. progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
  149. progress_template = self.get_param('progress_template', {})
  150. tmpl = progress_template.get('postprocess')
  151. if tmpl:
  152. self._downloader.to_screen(
  153. self._downloader.evaluate_outtmpl(tmpl, progress_dict), quiet=False)
  154. self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
  155. progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
  156. progress_dict))
  157. def _retry_download(self, err, count, retries):
  158. # While this is not an extractor, it behaves similar to one and
  159. # so obey extractor_retries and "--retry-sleep extractor"
  160. RetryManager.report_retry(err, count, retries, info=self.to_screen, warn=self.report_warning,
  161. sleep_func=self.get_param('retry_sleep_functions', {}).get('extractor'))
  162. def _download_json(self, url, *, expected_http_errors=(404,)):
  163. self.write_debug(f'{self.PP_NAME} query: {url}')
  164. for retry in RetryManager(self.get_param('extractor_retries', 3), self._retry_download):
  165. try:
  166. rsp = self._downloader.urlopen(Request(url))
  167. except network_exceptions as e:
  168. if isinstance(e, HTTPError) and e.status in expected_http_errors:
  169. return None
  170. retry.error = PostProcessingError(f'Unable to communicate with {self.PP_NAME} API: {e}')
  171. continue
  172. return json.loads(rsp.read().decode(rsp.headers.get_param('charset') or 'utf-8'))
  173. class AudioConversionError(PostProcessingError): # Deprecated
  174. pass