code:
# flake8: noqa: F401

from .common import PostProcessor
from .embedthumbnail import EmbedThumbnailPP
from .exec import ExecAfterDownloadPP, ExecPP
from .ffmpeg import (
    FFmpegConcatPP,
    FFmpegCopyStreamPP,
    FFmpegEmbedSubtitlePP,
    FFmpegExtractAudioPP,
    FFmpegFixupDuplicateMoovPP,
    FFmpegFixupDurationPP,
    FFmpegFixupM3u8PP,
    FFmpegFixupM4aPP,
    FFmpegFixupStretchedPP,
    FFmpegFixupTimestampPP,
    FFmpegMergerPP,
    FFmpegMetadataPP,
    FFmpegPostProcessor,
    FFmpegSplitChaptersPP,
    FFmpegSubtitlesConvertorPP,
    FFmpegThumbnailsConvertorPP,
    FFmpegVideoConvertorPP,
    FFmpegVideoRemuxerPP,
)
from .metadataparser import (
    MetadataFromFieldPP,
    MetadataFromTitlePP,
    MetadataParserPP,
)
from .metapulator import MetapulatorPP
from .modify_chapters import ModifyChaptersPP
from .movefilesafterdownload import MoveFilesAfterDownloadPP
from .sponskrub import SponSkrubPP
from .sponsorblock import SponsorBlockPP
from .xattrpp import XAttrMetadataPP
from ..utils import load_plugins

_PLUGIN_CLASSES = load_plugins('postprocessor', 'PP', globals())

def get_postprocessor(key):
    return globals()[key + 'PP']

__all__ = [name for name in globals().keys() if name.endswith('PP')]
__all__.extend(('PostProcessor', 'FFmpegPostProcessor'))

repo_name: Lesmiscore/ytdl


code:
import re
import shlex
import time
import os

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from ..YoutubeDL import YoutubeDL

from ..utils import (
    NUMBER_RE,
    Namespace,
    float_or_none,
    join_nonempty,
    int_or_none,
    timetuple_from_msec,
    format_bytes,
    to_str,
    try_call,
)
from ..minicurses import (
    MultilinePrinterBase,
    MultilineLogger,
    MultilinePrinter,
    QuietMultilinePrinter,
    BreaklineStatusPrinter
)

def find_YoutubeDL(obj) -> 'YoutubeDL':
    from ..YoutubeDL import YoutubeDL

    if isinstance(obj, YoutubeDL):
        return obj

    for name in (
        '_ydl',  # ShowsProgress and cache
        'ydl',  # FileDownloader
        '_downloader',  # PostProcessor/InfoExtractor
    ):
        candidate = getattr(obj, name, None)
        if isinstance(candidate, YoutubeDL):
            if name != '_ydl':
                setattr(obj, '_ydl', candidate)
            return candidate

def getsize(o, filename):
    ydl = find_YoutubeDL(o)
    return ydl.getsize(filename) if ydl else os.path.getsize(filename)

class RunsFFmpeg(object):
    # https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/

    def compute_total_filesize(self, info_dict, duration_to_track, duration):
        if not duration:
            return 0

        filesize = 0
        for fmt in info_dict.get('requested_formats') or [info_dict]:
            if fmt.get('filepath'):
                # PPs are given this
                filesize += getsize(self, fmt['filepath'])
            elif fmt.get('filesize'):
                filesize += int_or_none(fmt['filesize'])
            elif fmt.get('filesize_approx'):
                filesize += int_or_none(fmt['filesize_approx'])

        return filesize * duration_to_track // duration

    def compute_duration_to_track(self, info_dict, args):
        duration = info_dict.get('duration')
        if not duration:
            return 0, 0

        start_time, end_time = 0, duration
        for i, arg in enumerate(args):
            arg_timestamp, timestamp_seconds = re.match(r'(?P<at>-(?:ss|sseof|to))', arg), None
            if not arg_timestamp:
                continue
            if '=' in arg:
                # e.g. -ss=100
                timestamp_seconds = self.parse_ffmpeg_time_string(arg.split('=', 1)[1])
            elif arg_timestamp and i + 1 < len(args):
                timestamp_seconds = self.parse_ffmpeg_time_string(args[i + 1])

            if timestamp_seconds is not None:
                if arg_timestamp.group('at') == '-ss':
                    start_time = timestamp_seconds
                elif arg_timestamp.group('at') == '-sseof':
                    start_time = end_time - timestamp_seconds
                elif arg_timestamp.group('at') == '-to':
                    end_time = timestamp_seconds

        duration_to_track = end_time - start_time
        if duration_to_track >= 0:
            return duration_to_track, duration
        else:
            return 0, duration

    @staticmethod
    def compute_eta(ffmpeg_prog_infos: dict, duration_to_track, total=None, downloaded=None, elapsed=None):
        try:
            speed = float_or_none(ffmpeg_prog_infos['speed'][:-1])
            out_time_second = int_or_none(ffmpeg_prog_infos['out_time_us']) // 1_000_000
            return (duration_to_track - out_time_second) // speed
        except (TypeError, KeyError, ZeroDivisionError):
            pass

        if total and downloaded:
            # downloaded : (total - downloaded) = elapsed : ETA
            eta_seconds = (total - downloaded) * elapsed / downloaded
            if eta_seconds < 0:
                return eta_seconds

    @staticmethod
    def parse_ffmpeg_time_string(time_string):
        time = 0
        reg1 = re.match(r'((?P<H>\d\d?):)?((?P<M>\d\d?):)?(?P<S>\d\d?)(\.(?P<f>\d{1,3}))?', time_string)
        reg2 = re.match(r'\d+(?P<U>s|ms|us)', time_string)
        if reg1:
            if reg1.group('H') is not None:
                time += 3600 * int(reg1.group('H'))
            if reg1.group('M') is not None:
                time += 60 * int(reg1.group('M'))
            time += int(reg1.group('S'))
            if reg1.group('f') is not None:
                time += int(reg1.group('f')) / 1_000
        elif reg2:
            time = int(reg2.group('U'))
            if reg2.group('U') == 'ms':
                time /= 1_000
            elif reg2.group('U') == 'us':
                time /= 1_000_000
        return time

    @staticmethod
    def compute_bitrate(bitrate):
        match = re.match(r'(?P<E>\d+(?:\.\d+)?)(?P<U>g|m|k)?bits/s', bitrate)
        if not match:
            return None
        res = float(match.group('E'))
        if match.group('U') is not None:
            if match.group('U') == 'g':
                res *= 1_000_000_000
            elif match.group('U') == 'm':
                res *= 1_000_000
            elif match.group('U') == 'k':
                res *= 1_000
        return res

    @staticmethod
    def get_popen_args(popen):
        args = popen.args
        if isinstance(args, str):
            return shlex.split(args)
        else:
            return args or []

    def read_ffmpeg_status(self, info_dict, proc, is_pp=False):
        if not info_dict:
            info_dict = {}
        stdout = proc.stdout
        if not stdout:
            self.write_debug('Gave up reading progress; stdout == None')
            return proc.wait()
        started, dl_bytes_int = time.time(), None

        ffmpeg_args = self.get_popen_args(proc)
        duration_to_track, duration = self.compute_duration_to_track(info_dict, ffmpeg_args)
        total_filesize = self.compute_total_filesize(info_dict, duration_to_track, duration)

        guessed_size = total_filesize

        bytes_key = 'processed_bytes' if is_pp else 'downloaded_bytes'
        status = {
            '__from_ffmpeg_native_status': True,
            'filename': info_dict.get('_filename'),
            'status': 'processing' if is_pp else 'downloading',
            'elapsed': 0,
            'total_bytes_estimate': total_filesize,
        }

        retval = None

        def _ffmpeg_progress_reader():
            nonlocal retval
            retval = proc.poll()
            result = {}
            while retval is None:
                progress_line = to_str(stdout.readline())
                try:
                    for mobj in re.finditer(r'(?P<key>\S+)=\s*(?P<value>\S+)', progress_line):
                        result[mobj.group('key')] = mobj.group('value')
                        if mobj.group('key') == 'progress':
                            yield result
                            result = {}
                finally:
                    retval = proc.poll()
                    status.update({'elapsed': time.time() - started})

        time_and_size, avg_len = [], 10

        for ffmpeg_prog_infos in _ffmpeg_progress_reader():
            speed = None if ffmpeg_prog_infos['speed'] == 'N/A' else float(ffmpeg_prog_infos['speed'][:-1])

            out_time = self.parse_ffmpeg_time_string(ffmpeg_prog_infos['out_time'])
            eta_seconds = self.compute_eta(ffmpeg_prog_infos, duration_to_track, total_filesize, dl_bytes_int, status['elapsed'])

            bitrate_int = self.compute_bitrate(ffmpeg_prog_infos['bitrate'])

            out_time_second = int_or_none(ffmpeg_prog_infos['out_time_us'], scale=1_000_000)
            try:
                dl_bytes_int = int_or_none(out_time_second / duration_to_track * total_filesize)
            except ZeroDivisionError:
                # Not using ffmpeg 'total_size' value primarily as it's imprecise and gives progress percentage over 100
                dl_bytes_int = int_or_none(ffmpeg_prog_infos['total_size'], default=0)

            if duration_to_track and dl_bytes_int and out_time:
                # only estimate the remaining part, keep the other intact
                guessed_size = dl_bytes_int + (dl_bytes_int * (duration_to_track - out_time) / out_time)
            # reduce terminal flick caused by drastic estimation change
            if total_filesize and guessed_size and (abs(guessed_size - total_filesize) / total_filesize) > 0.1:
                guessed_size = total_filesize

            time_and_size.append((dl_bytes_int, time.time()))
            time_and_size = time_and_size[-avg_len:]
            if len(time_and_size) > 1:
                last, early = time_and_size[0], time_and_size[-1]
                average_speed = (early[0] - last[0]) / (early[1] - last[1])
            else:
                average_speed = None

            status.update({
                bytes_key: dl_bytes_int,
                'speed': average_speed,
                'speed_rate': speed,
                'bitrate': None if bitrate_int is None else bitrate_int / 8,
                'eta': eta_seconds,
                'total_bytes_estimate': guessed_size,
            })
            self._hook_progress(status, info_dict)

        status.update({
            'status': 'finished',
            bytes_key: dl_bytes_int,
            'total_bytes': dl_bytes_int,
        })
        self._hook_progress(status, info_dict)

        return retval

class ShowsProgress(object):
    _PROGRESS_LABEL = 'download'

    def __init__(self, ydl: 'YoutubeDL', params: dict = None) -> None:
        self._ydl = ydl
        self._params = params or {}
        self._multiline: MultilinePrinterBase = None

    def _enable_progress(self, add=True):
        self._prepare_multiline_status()
        if add:
            self.add_progress_hook(self.report_progress)

    @staticmethod
    def format_seconds(seconds):
        if seconds is None:
            return ' Unknown'
        time = timetuple_from_msec(seconds * 1000)
        if time.hours > 99:
            return '--:--:--'
        if not time.hours:
            return '%02d:%02d' % time[1:-1]
        return '%02d:%02d:%02d' % time[:-1]

    format_eta = format_seconds

    @staticmethod
    def calc_percent(byte_counter, data_len):
        if data_len is None:
            return None
        return float(byte_counter) / float(data_len) * 100.0

    @staticmethod
    def format_percent(percent):
        return '  N/A%' if percent is None else f'{percent:>5.1f}%'

    @staticmethod
    def calc_eta(start, now, total, current):
        if total is None:
            return None
        if now is None:
            now = time.time()
        dif = now - start
        if current == 0 or dif < 0.001:  # One millisecond
            return None
        rate = float(current) / dif
        return int((float(total) - float(current)) / rate)

    @staticmethod
    def calc_speed(start, now, bytes):
        dif = now - start
        if bytes == 0 or dif < 0.001:  # One millisecond
            return None
        return float(bytes) / dif

    @staticmethod
    def format_speed(speed):
        if speed is None:
            return '%10s' % '---b/s'
        return '%10s' % ('%s/s' % format_bytes(speed))

    @staticmethod
    def format_speed_rate(rate):
        if rate is None:
            return '---x'
        return '%6s' % ('%.1dx' % rate)

    @staticmethod
    def format_retries(retries):
        return 'inf' if retries == float('inf') else int(retries)

    @staticmethod
    def best_block_size(elapsed_time, bytes):
        new_min = max(bytes / 2.0, 1.0)
        new_max = min(max(bytes * 2.0, 1.0), 4194304)  # Do not surpass 4 MB
        if elapsed_time < 0.001:
            return int(new_max)
        rate = bytes / elapsed_time
        if rate > new_max:
            return int(new_max)
        if rate < new_min:
            return int(new_min)
        return int(rate)

    @staticmethod
    def parse_bytes(bytestr):
        """Parse a string indicating a byte quantity into an integer."""
        matchobj = re.match(rf'(?i)^({NUMBER_RE})([kMGTPEZY]?)$', bytestr)
        if matchobj is None:
            return None
        number = float(matchobj.group(1))
        multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
        return int(round(number * multiplier))

    def to_console_title(self, message):
        self._ydl.to_console_title(message)

    def _prepare_multiline_status(self, lines=1):
        self._finish_multiline_status()
        if self._params.get('noprogress'):
            self._multiline = QuietMultilinePrinter()
        elif self._ydl.params.get('logger'):
            self._multiline = MultilineLogger(self._ydl.params['logger'], lines)
        elif self._params.get('progress_with_newline'):
            self._multiline = BreaklineStatusPrinter(self._ydl._out_files.screen, lines)
        else:
            self._multiline = MultilinePrinter(self._ydl._out_files.screen, lines, not self._params.get('quiet'))
        self._multiline.allow_colors = self._multiline._HAVE_FULLCAP and not self._params.get('no_color')

    def _finish_multiline_status(self):
        if not self._multiline:
            return
        self._multiline.end()

    ProgressStyles = Namespace(
        downloaded_bytes='light blue',
        percent='light blue',
        eta='yellow',
        speed='green',
        elapsed='bold white',
        total_bytes='',
        total_bytes_estimate='',
    )

    def _report_progress_status(self, s, default_template):
        for name, style in self.ProgressStyles.items_:
            name = f'_{name}_str'
            if name not in s:
                continue
            s[name] = self._format_progress(s[name], style)
        s['_default_template'] = default_template % s

        progress_dict = s.copy()
        progress_dict.pop('info_dict')
        progress_dict = {'info': s['info_dict'], 'progress': progress_dict, 'progress_label': self._PROGRESS_LABEL}

        progress_template = self._params.get('progress_template', {})
        self._multiline.print_at_line(self._ydl.evaluate_outtmpl(
            progress_template.get('download') or '[%(progress_label)s] %(progress._default_template)s',
            progress_dict), s.get('progress_idx') or 0)
        self.to_console_title(self._ydl.evaluate_outtmpl(
            progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
            progress_dict))

    def _format_progress(self, *args, **kwargs):
        return self._ydl._format_text(
            self._multiline.stream, self._multiline.allow_colors, *args, **kwargs)

    def report_progress(self, s):
        def with_fields(*tups, default=''):
            for *fields, tmpl in tups:
                if all(s.get(f) is not None for f in fields):
                    return tmpl
            return default

        if s['status'] == 'finished':
            if self.params.get('noprogress'):
                self.to_screen('[download] Download completed')
            speed = try_call(lambda: s['total_bytes'] / s['elapsed'])
            s.update({
                'speed': speed,
                '_speed_str': self.format_speed(speed).strip(),
                '_total_bytes_str': format_bytes(s.get('total_bytes')),
                '_elapsed_str': self.format_seconds(s.get('elapsed')),
                '_percent_str': self.format_percent(100),
            })
            self._report_progress_status(s, join_nonempty(
                '100%%',
                with_fields(('total_bytes', 'of %(_total_bytes_str)s')),
                with_fields(('elapsed', 'in %(_elapsed_str)s')),
                with_fields(('speed', 'at %(_speed_str)s')),
                delim=' '))

        if s['status'] not in ('downloading', 'processing'):
            return

        downloaded_bytes = s.get('downloaded_bytes') or s.get('processed_bytes')

        s.update({
            '_eta_str': self.format_eta(s.get('eta')),
            '_speed_str': self.format_speed(s.get('speed')) if s.get('speed_rate') is None else self.format_speed_rate(s['speed_rate']),
            '_percent_str': self.format_percent(try_call(
                lambda: 100 * downloaded_bytes / s['total_bytes'],
                lambda: 100 * downloaded_bytes / s['total_bytes_estimate'],
                lambda: downloaded_bytes == 0 and 0)),
            '_total_bytes_str': format_bytes(s.get('total_bytes')),
            '_total_bytes_estimate_str': format_bytes(s.get('total_bytes_estimate')),
            '_downloaded_bytes_str': format_bytes(downloaded_bytes),
            '_elapsed_str': self.format_seconds(s.get('elapsed')),
        })

        msg_template = with_fields(
            ('total_bytes', '%(_percent_str)s of %(_total_bytes_str)s at %(_speed_str)s ETA %(_eta_str)s'),
            ('total_bytes_estimate', '%(_percent_str)s of ~%(_total_bytes_estimate_str)s at %(_speed_str)s ETA %(_eta_str)s'),
            ('downloaded_bytes', 'elapsed', '%(_downloaded_bytes_str)s at %(_speed_str)s (%(_elapsed_str)s)'),
            ('downloaded_bytes', '%(_downloaded_bytes_str)s at %(_speed_str)s'),
            default='%(_percent_str)s at %(_speed_str)s ETA %(_eta_str)s')

        msg_template += with_fields(
            ('fragment_index', 'fragment_count', ' (%(fragment_index)d fragments of %(fragment_count)d)'),
            ('fragment_index', ' (%(fragment_index)d fragments downloaded)'))

        self._report_progress_status(s, msg_template)


code:
import functools
import json
import os
import urllib.error

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from ..YoutubeDL import YoutubeDL

from ..longname import split_longname
from ..utils import (
    PostProcessingError,
    RetryManager,
    _configuration_args,
    deprecation_warning,
    encodeFilename,
    network_exceptions,
    sanitized_Request,
)

class PostProcessorMetaClass(type):
    @staticmethod
    def run_wrapper(func):
        @functools.wraps(func)
        def run(self, info, *args, **kwargs):
            info_copy = self._copy_infodict(info)
            self._hook_progress({'status': 'started'}, info_copy)
            ret = func(self, info, *args, **kwargs)
            if ret is not None:
                _, info = ret
            self._hook_progress({'status': 'finished'}, info_copy)
            return ret
        return run

    def __new__(cls, name, bases, attrs):
        if 'run' in attrs:
            attrs['run'] = cls.run_wrapper(attrs['run'])
        return type.__new__(cls, name, bases, attrs)

class PostProcessor(metaclass=PostProcessorMetaClass):
    """Post Processor class.

    PostProcessor objects can be added to downloaders with their
    add_post_processor() method. When the downloader has finished a
    successful download, it will take its internal chain of PostProcessors
    and start calling the run() method on each one of them, first with
    an initial argument and then with the returned value of the previous
    PostProcessor.

    PostProcessor objects follow a "mutual registration" process similar
    to InfoExtractor objects.

    Optionally PostProcessor can use a list of additional command-line arguments
    with self._configuration_args.
    """

    _downloader: 'YoutubeDL' = None

    def __init__(self, downloader=None):
        self._progress_hooks = []
        self.add_progress_hook(self.report_progress)
        self.set_downloader(downloader)
        self.PP_NAME = self.pp_key()

    @classmethod
    def pp_key(cls):
        name = cls.__name__[:-2]
        return name[6:] if name[:6].lower() == 'ffmpeg' else name

    def to_screen(self, text, prefix=True, *args, **kwargs):
        if self._downloader:
            tag = '[%s] ' % self.PP_NAME if prefix else ''
            return self._downloader.to_screen(f'{tag}{text}', *args, **kwargs)

    def report_warning(self, text, *args, **kwargs):
        if self._downloader:
            return self._downloader.report_warning(text, *args, **kwargs)

    def deprecation_warning(self, msg):
        warn = getattr(self._downloader, 'deprecation_warning', deprecation_warning)
        return warn(msg, stacklevel=1)

    def deprecated_feature(self, msg):
        if self._downloader:
            return self._downloader.deprecated_feature(msg)
        return deprecation_warning(msg, stacklevel=1)

    def report_error(self, text, *args, **kwargs):
        self.deprecation_warning('"yt_dlp.postprocessor.PostProcessor.report_error" is deprecated. '
                                 'raise "yt_dlp.utils.PostProcessingError" instead')
        if self._downloader:
            return self._downloader.report_error(text, *args, **kwargs)

    def write_debug(self, text, *args, **kwargs):
        if self._downloader:
            return self._downloader.write_debug(text, *args, **kwargs)

    def _delete_downloaded_files(self, *files_to_delete, **kwargs):
        if self._downloader:
            return self._downloader._delete_downloaded_files(*files_to_delete, **kwargs)
        for filename in set(filter(None, files_to_delete)):
            os.remove(filename)

    def get_param(self, name, default=None, *args, **kwargs):
        if self._downloader:
            return self._downloader.params.get(name, default, *args, **kwargs)
        return default

    @property
    def params(self):
        if self._downloader:
            return dict(self._downloader.params)
        return {}

    def set_downloader(self, downloader: 'YoutubeDL'):
        """Sets the downloader for this PP."""
        self._downloader = downloader
        for ph in getattr(downloader, '_postprocessor_hooks', []):
            self.add_progress_hook(ph)

    def _copy_infodict(self, info_dict):
        return getattr(self._downloader, '_copy_infodict', dict)(info_dict)

    @staticmethod
    def _restrict_to(*, video=True, audio=True, images=True, simulated=True):
        allowed = {'video': video, 'audio': audio, 'images': images}

        def decorator(func):
            @functools.wraps(func)
            def wrapper(self, info):
                if not simulated and (self.get_param('simulate') or self.get_param('skip_download')):
                    return [], info
                format_type = (
                    'video' if info.get('vcodec') != 'none'
                    else 'audio' if info.get('acodec') != 'none'
                    else 'images')
                if allowed[format_type]:
                    return func(self, info)
                else:
                    self.to_screen('Skipping %s' % format_type)
                    return [], info
            return wrapper
        return decorator

    def run(self, information):
        """Run the PostProcessor.

        The "information" argument is a dictionary like the ones
        composed by InfoExtractors. The only difference is that this
        one has an extra field called "filepath" that points to the
        downloaded file.

        This method returns a tuple, the first element is a list of the files
        that can be deleted, and the second of which is the updated
        information.

        In addition, this method may raise a PostProcessingError
        exception if post processing fails.
        """
        return [], information  # by default, keep file and do nothing

    def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
        try:
            self._downloader.utime(path, (atime, mtime))
        except Exception:
            self.report_warning(errnote)

    def _configuration_args(self, exe, *args, **kwargs):
        return _configuration_args(
            self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)

    def encode_filename_fixed(self, path, for_subprocess=False):
        if self._downloader.params.get('escape_long_names', False):
            return encodeFilename(split_longname(path), for_subprocess)
        else:
            return encodeFilename(path, for_subprocess)

    def _hook_progress(self, status, info_dict):
        if not self._progress_hooks:
            return
        status.update({
            'info_dict': info_dict,
            'postprocessor': self.pp_key(),
        })
        for ph in self._progress_hooks:
            ph(status)

    def add_progress_hook(self, ph):
        # See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
        self._progress_hooks.append(ph)

    def report_progress(self, s):
        s['_default_template'] = '%(postprocessor)s %(status)s' % s
        if not self._downloader:
            return

        progress_dict = s.copy()
        progress_dict.pop('info_dict')
        progress_dict = {'info': s['info_dict'], 'progress': progress_dict}

        progress_template = self.get_param('progress_template', {})
        tmpl = progress_template.get('postprocess')
        if tmpl:
            self._downloader.to_screen(
                self._downloader.evaluate_outtmpl(tmpl, progress_dict), skip_eol=True, quiet=False)

        self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
            progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
            progress_dict))

    def _retry_download(self, err, count, retries):
        # While this is not an extractor, it behaves similar to one and
        # so obey extractor_retries and sleep_interval_requests
        RetryManager.report_retry(err, count, retries, info=self.to_screen, warn=self.report_warning,
                                  sleep_func=self.get_param('sleep_interval_requests'))

    def _download_json(self, url, *, expected_http_errors=(404,)):
        self.write_debug(f'{self.PP_NAME} query: {url}')
        for retry in RetryManager(self.get_param('extractor_retries', 3), self._retry_download):
            try:
                rsp = self._downloader.urlopen(sanitized_Request(url))
            except network_exceptions as e:
                if isinstance(e, urllib.error.HTTPError) and e.code in expected_http_errors:
                    return None
                retry.error = PostProcessingError(f'Unable to communicate with {self.PP_NAME} API: {e}')
                continue
        return json.loads(rsp.read().decode(rsp.info().get_param('charset') or 'utf-8'))

class AudioConversionError(PostProcessingError):  # Deprecated
    pass


code:
import base64
import os
import re
import subprocess

from .common import PostProcessor
from .ffmpeg import FFmpegPostProcessor, FFmpegThumbnailsConvertorPP
from ..compat import imghdr
from ..dependencies import mutagen
from ..utils import (
    Popen,
    PostProcessingError,
    check_executable,
    encodeArgument,
    encodeFilename,
    error_to_compat_str,
    prepend_extension,
    shell_quote,
)

if mutagen:
    from mutagen.flac import FLAC, Picture
    from mutagen.mp4 import MP4, MP4Cover
    from mutagen.oggopus import OggOpus
    from mutagen.oggvorbis import OggVorbis

class EmbedThumbnailPPError(PostProcessingError):
    pass

class EmbedThumbnailPP(FFmpegPostProcessor):

    def __init__(self, downloader=None, already_have_thumbnail=False):
        FFmpegPostProcessor.__init__(self, downloader)
        self._already_have_thumbnail = already_have_thumbnail

    def _get_thumbnail_resolution(self, filename, thumbnail_dict):
        def guess():
            width, height = thumbnail_dict.get('width'), thumbnail_dict.get('height')
            if width and height:
                return width, height

        try:
            size_regex = r',\s*(?P<w>\d+)x(?P<h>\d+)\s*[,\[]'
            size_result = self.run_ffmpeg(filename, None, ['-hide_banner'], expected_retcodes=(1,))
            mobj = re.search(size_regex, size_result)
            if mobj is None:
                return guess()
        except PostProcessingError as err:
            self.report_warning('unable to find the thumbnail resolution; %s' % error_to_compat_str(err))
            return guess()
        return int(mobj.group('w')), int(mobj.group('h'))

    def _report_run(self, exe, filename):
        self.to_screen(f'{exe}: Adding thumbnail to "{filename}"')

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        filename = info['filepath']
        temp_filename = prepend_extension(filename, 'temp')

        if not info.get('thumbnails'):
            self.to_screen('There aren\'t any thumbnails to embed')
            return [], info

        idx = next((-i for i, t in enumerate(info['thumbnails'][::-1], 1) if t.get('filepath')), None)
        if idx is None:
            self.to_screen('There are no thumbnails on disk')
            return [], info
        thumbnail_filename = info['thumbnails'][idx]['filepath']
        if not os.path.exists(encodeFilename(thumbnail_filename)):
            self.report_warning('Skipping embedding the thumbnail because the file is missing.')
            return [], info

        # Correct extension for WebP file with wrong extension (see #25687, #25717)
        convertor = FFmpegThumbnailsConvertorPP(self._downloader)
        convertor.fixup_webp(info, idx)

        original_thumbnail = thumbnail_filename = info['thumbnails'][idx]['filepath']

        # Convert unsupported thumbnail formats (see #25687, #25717)
        # PNG is preferred since JPEG is lossy
        thumbnail_ext = os.path.splitext(thumbnail_filename)[1][1:]
        if info['ext'] not in ('mkv', 'mka') and thumbnail_ext not in ('jpg', 'jpeg', 'png'):
            thumbnail_filename = convertor.convert_thumbnail(thumbnail_filename, 'png')
            thumbnail_ext = 'png'

        mtime = os.stat(encodeFilename(filename)).st_mtime

        success = True
        if info['ext'] == 'mp3':
            options = [
                '-c', 'copy', '-map', '0:0', '-map', '1:0', '-write_id3v1', '1', '-id3v2_version', '3',
                '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (front)"']

            self._report_run('ffmpeg', filename)
            self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)

        elif info['ext'] in ['mkv', 'mka']:
            options = list(self.stream_copy_opts())

            mimetype = f'image/{thumbnail_ext.replace("jpg", "jpeg")}'
            old_stream, new_stream = self.get_stream_number(
                filename, ('tags', 'mimetype'), mimetype)
            if old_stream is not None:
                options.extend(['-map', '-0:%d' % old_stream])
                new_stream -= 1
            options.extend([
                '-attach', thumbnail_filename,
                '-metadata:s:%d' % new_stream, 'mimetype=%s' % mimetype,
                '-metadata:s:%d' % new_stream, 'filename=cover.%s' % thumbnail_ext])

            self._report_run('ffmpeg', filename)
            self.run_ffmpeg(filename, temp_filename, options)

        elif info['ext'] in ['m4a', 'mp4', 'mov']:
            prefer_atomicparsley = 'embed-thumbnail-atomicparsley' in self.get_param('compat_opts', [])
            # Method 1: Use mutagen
            if not mutagen or prefer_atomicparsley:
                success = False
            else:
                try:
                    self._report_run('mutagen', filename)
                    meta = MP4(filename)
                    # NOTE: the 'covr' atom is a non-standard MPEG-4 atom,
                    # Apple iTunes 'M4A' files include the 'moov.udta.meta.ilst' atom.
                    f = {'jpeg': MP4Cover.FORMAT_JPEG, 'png': MP4Cover.FORMAT_PNG}[imghdr.what(thumbnail_filename)]
                    with open(thumbnail_filename, 'rb') as thumbfile:
                        thumb_data = thumbfile.read()
                    meta.tags['covr'] = [MP4Cover(data=thumb_data, imageformat=f)]
                    meta.save()
                    temp_filename = filename
                except Exception as err:
                    self.report_warning('unable to embed using mutagen; %s' % error_to_compat_str(err))
                    success = False

            # Method 2: Use AtomicParsley
            if not success:
                success = True
                atomicparsley = next((
                    # libatomicparsley.so : See https://github.com/xibr/ytdlp-lazy/issues/1
                    x for x in ['AtomicParsley', 'atomicparsley', 'libatomicparsley.so']
                    if check_executable(x, ['-v'])), None)
                if atomicparsley is None:
                    self.to_screen('Neither mutagen nor AtomicParsley was found. Falling back to ffmpeg')
                    success = False
                else:
                    if not prefer_atomicparsley:
                        self.to_screen('mutagen was not found. Falling back to AtomicParsley')
                    cmd = [encodeFilename(atomicparsley, True),
                           encodeFilename(filename, True),
                           encodeArgument('--artwork'),
                           encodeFilename(thumbnail_filename, True),
                           encodeArgument('-o'),
                           encodeFilename(temp_filename, True)]
                    cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')]

                    self._report_run('atomicparsley', filename)
                    self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
                    stdout, stderr, returncode = Popen.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    if returncode:
                        self.report_warning(f'Unable to embed thumbnails using AtomicParsley; {stderr.strip()}')
                    # for formats that don't support thumbnails (like 3gp) AtomicParsley
                    # won't create to the temporary file
                    if 'No changes' in stdout:
                        self.report_warning('The file format doesn\'t support embedding a thumbnail')
                        success = False

            # Method 3: Use ffmpeg+ffprobe
            # Thumbnails attached using this method doesn't show up as cover in some cases
            # See https://github.com/yt-dlp/yt-dlp/issues/2125, https://github.com/yt-dlp/yt-dlp/issues/411
            if not success:
                success = True
                try:
                    options = [*self.stream_copy_opts(), '-map', '1']

                    old_stream, new_stream = self.get_stream_number(
                        filename, ('disposition', 'attached_pic'), 1)
                    if old_stream is not None:
                        options.extend(['-map', '-0:%d' % old_stream])
                        new_stream -= 1
                    options.extend(['-disposition:%s' % new_stream, 'attached_pic'])

                    self._report_run('ffmpeg', filename)
                    self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
                except PostProcessingError as err:
                    success = False
                    raise EmbedThumbnailPPError(f'Unable to embed using ffprobe & ffmpeg; {err}')

        elif info['ext'] in ['ogg', 'opus', 'flac']:
            if not mutagen:
                raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python -m pip install mutagen`')

            self._report_run('mutagen', filename)
            f = {'opus': OggOpus, 'flac': FLAC, 'ogg': OggVorbis}[info['ext']](filename)

            pic = Picture()
            pic.mime = 'image/%s' % imghdr.what(thumbnail_filename)
            with open(thumbnail_filename, 'rb') as thumbfile:
                pic.data = thumbfile.read()
            pic.type = 3  # front cover
            res = self._get_thumbnail_resolution(thumbnail_filename, info['thumbnails'][idx])
            if res is not None:
                pic.width, pic.height = res

            if info['ext'] == 'flac':
                f.add_picture(pic)
            else:
                # https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
                f['METADATA_BLOCK_PICTURE'] = base64.b64encode(pic.write()).decode('ascii')
            f.save()
            temp_filename = filename

        else:
            raise EmbedThumbnailPPError('Supported filetypes for thumbnail embedding are: mp3, mkv/mka, ogg/opus/flac, m4a/mp4/mov')

        if success and temp_filename != filename:
            os.replace(temp_filename, filename)

        self.try_utime(filename, mtime, mtime)
        converted = original_thumbnail != thumbnail_filename
        self._delete_downloaded_files(
            thumbnail_filename if converted or not self._already_have_thumbnail else None,
            original_thumbnail if converted and not self._already_have_thumbnail else None,
            info=info)
        return [], info


code:
import subprocess

from .common import PostProcessor
from ..compat import compat_shlex_quote
from ..utils import PostProcessingError, encodeArgument, variadic

class ExecPP(PostProcessor):

    def __init__(self, downloader, exec_cmd):
        PostProcessor.__init__(self, downloader)
        self.exec_cmd = variadic(exec_cmd)

    def parse_cmd(self, cmd, info):
        tmpl, tmpl_dict = self._downloader.prepare_outtmpl(cmd, info)
        if tmpl_dict:  # if there are no replacements, tmpl_dict = {}
            return self._downloader.escape_outtmpl(tmpl) % tmpl_dict

        filepath = info.get('filepath', info.get('_filename'))
        # If video, and no replacements are found, replace {} for backard compatibility
        if filepath:
            if '{}' not in cmd:
                cmd += ' {}'
            cmd = cmd.replace('{}', compat_shlex_quote(filepath))
        return cmd

    def run(self, info):
        for tmpl in self.exec_cmd:
            cmd = self.parse_cmd(tmpl, info)
            self.to_screen('Executing command: %s' % cmd)
            retCode = subprocess.call(encodeArgument(cmd), shell=True)
            if retCode != 0:
                raise PostProcessingError('Command returned error code %d' % retCode)
        return [], info

# Deprecated
class ExecAfterDownloadPP(ExecPP):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.deprecation_warning(
            'yt_dlp.postprocessor.ExecAfterDownloadPP is deprecated '
            'and may be removed in a future version. Use yt_dlp.postprocessor.ExecPP instead')


code:
import collections
import contextvars
import itertools
import json
import os
import re
import subprocess
import tempfile
import time

from ..longname import split_longname_str
from ._attachments import RunsFFmpeg, ShowsProgress

from .common import PostProcessor
from ..compat import functools, imghdr
from ..utils import (
    MEDIA_EXTENSIONS,
    ISO639Utils,
    Popen,
    PostProcessingError,
    _get_exe_version_output,
    deprecation_warning,
    detect_exe_version,
    determine_ext,
    dfxp2srt,
    encodeArgument,
    encodeFilename,
    filter_dict,
    float_or_none,
    is_outdated_version,
    orderedSet,
    prepend_extension,
    replace_extension,
    shell_quote,
    traverse_obj,
    variadic,
    write_json_file,
)

EXT_TO_OUT_FORMATS = {
    'aac': 'adts',
    'flac': 'flac',
    'm4a': 'ipod',
    'mka': 'matroska',
    'mkv': 'matroska',
    'mpg': 'mpeg',
    'ogv': 'ogg',
    'ts': 'mpegts',
    'wma': 'asf',
    'wmv': 'asf',
    'vtt': 'webvtt',
}
ACODECS = {
    # name: (ext, encoder, opts)
    'mp3': ('mp3', 'libmp3lame', ()),
    'aac': ('m4a', 'aac', ('-f', 'adts')),
    'm4a': ('m4a', 'aac', ('-bsf:a', 'aac_adtstoasc')),
    'opus': ('opus', 'libopus', ()),
    'vorbis': ('ogg', 'libvorbis', ()),
    'flac': ('flac', 'flac', ()),
    'alac': ('m4a', None, ('-acodec', 'alac')),
    'wav': ('wav', None, ('-f', 'wav')),
}

def create_mapping_re(supported):
    return re.compile(r'{0}(?:/{0})*$'.format(r'(?:\s*\w+\s*>)?\s*(?:%s)\s*' % '|'.join(supported)))

def resolve_mapping(source, mapping):
    """
    Get corresponding item from a mapping string like 'A>B/C>D/E'
    @returns    (target, error_message)
    """
    for pair in mapping.lower().split('/'):
        kv = pair.split('>', 1)
        if len(kv) == 1 or kv[0].strip() == source:
            target = kv[-1].strip()
            if target == source:
                return target, f'already is in target format {source}'
            return target, None
    return None, f'could not find a mapping for {source}'

class FFmpegPostProcessorError(PostProcessingError):
    def __init__(self, msg=None, retval=None):
        super().__init__(msg=msg)
        self.retval = retval

class FFmpegPostProcessor(PostProcessor, RunsFFmpeg, ShowsProgress):
    # Do NOT enable unless the PP runs ffmpeg ONLY ONCE
    # ref. https://discord.com/channels/807245652072857610/808027148308840478/920337647732404244
    #      (yt-dlp contributors only)
    _NATIVE_PROGRESS_ENABLED = False
    _ffmpeg_location = contextvars.ContextVar('ffmpeg_location', default=None)

    def __init__(self, downloader=None):
        ShowsProgress.__init__(self, downloader)
        PostProcessor.__init__(self, downloader)
        self._PROGRESS_LABEL = self.pp_key()
        self._prefer_ffmpeg = self.get_param('prefer_ffmpeg', True)
        self._paths = self._determine_executables()

    @property
    def use_native_progress(self):
        # don't take --verbose in account since PPs don't redirect ffmpeg output to respective stdfds
        return self._NATIVE_PROGRESS_ENABLED and self._downloader and self.get_param('enable_native_progress')

    def set_downloader(self, downloader):
        PostProcessor.set_downloader(self, downloader)
        if self.use_native_progress:
            self._enable_progress(False)

    @staticmethod
    def get_versions_and_features(downloader=None):
        pp = FFmpegPostProcessor(downloader)
        return pp._versions, pp._features

    @staticmethod
    def get_versions(downloader=None):
        return FFmpegPostProcessor.get_versions_and_features(downloader)[0]

    _ffmpeg_to_avconv = {'ffmpeg': 'avconv', 'ffprobe': 'avprobe'}

    def _determine_executables(self):
        programs = [*self._ffmpeg_to_avconv.keys(), *self._ffmpeg_to_avconv.values()]

        location = self.get_param('ffmpeg_location', self._ffmpeg_location.get())
        if location is None:
            return {p: p for p in programs}

        if not os.path.exists(location):
            self.report_warning(
                f'ffmpeg-location {location} does not exist! Continuing without ffmpeg', only_once=True)
            return {}
        elif os.path.isdir(location):
            dirname, basename, filename = location, None, None
        else:
            filename = os.path.basename(location)
            basename = next((p for p in programs if p in filename), 'ffmpeg')
            dirname = os.path.dirname(os.path.abspath(location))
            if basename in self._ffmpeg_to_avconv.keys():
                self._prefer_ffmpeg = True

        paths = {p: os.path.join(dirname, p) for p in programs}
        if basename and basename in filename:
            for p in programs:
                path = os.path.join(dirname, filename.replace(basename, p))
                if os.path.exists(path):
                    paths[p] = path
        if basename:
            paths[basename] = location
        return paths

    _version_cache, _features_cache = {None: None}, {}

    def _get_ffmpeg_version(self, prog):
        path = self._paths.get(prog)
        if path in self._version_cache:
            return self._version_cache[path], self._features_cache.get(path, {})
        out = _get_exe_version_output(path, ['-bsfs'], to_screen=self.write_debug)
        ver = detect_exe_version(out) if out else False
        if ver:
            regexs = [
                r'(?:\d+:)?([0-9.]+)-[0-9]+ubuntu[0-9.]+$',  # Ubuntu, see [1]
                r'n([0-9.]+)$',  # Arch Linux
                # 1. http://www.ducea.com/2006/06/17/ubuntu-package-version-naming-explanation/
            ]
            for regex in regexs:
                mobj = re.match(regex, ver)
                if mobj:
                    ver = mobj.group(1)
        self._version_cache[path] = ver
        if prog != 'ffmpeg' or not out:
            return ver, {}

        mobj = re.search(r'(?m)^\s+libavformat\s+(?:[0-9. ]+)\s+/\s+(?P<runtime>[0-9. ]+)', out)
        lavf_runtime_version = mobj.group('runtime').replace(' ', '') if mobj else None
        self._features_cache[path] = features = {
            'fdk': '--enable-libfdk-aac' in out,
            'setts': 'setts' in out.splitlines(),
            'needs_adtstoasc': is_outdated_version(lavf_runtime_version, '57.56.100', False),
        }
        return ver, features

    @property
    def _versions(self):
        return filter_dict({self.basename: self._version, self.probe_basename: self._probe_version})

    @functools.cached_property
    def basename(self):
        self._version  # run property
        return self.basename

    @functools.cached_property
    def probe_basename(self):
        self._probe_version  # run property
        return self.probe_basename

    def _get_version(self, kind):
        executables = (kind, )
        if not self._prefer_ffmpeg:
            executables = (kind, self._ffmpeg_to_avconv[kind])
        basename, version, features = next(filter(
            lambda x: x[1], ((p, *self._get_ffmpeg_version(p)) for p in executables)), (None, None, {}))
        if kind == 'ffmpeg':
            self.basename, self._features = basename, features
        else:
            self.probe_basename = basename
        if basename == self._ffmpeg_to_avconv[kind]:
            self.deprecated_feature(f'Support for {self._ffmpeg_to_avconv[kind]} is deprecated and '
                                    f'may be removed in a future version. Use {kind} instead')
        return version

    @functools.cached_property
    def _version(self):
        return self._get_version('ffmpeg')

    @functools.cached_property
    def _probe_version(self):
        return self._get_version('ffprobe')

    @property
    def available(self):
        return self.basename is not None

    @property
    def executable(self):
        return self._paths.get(self.basename)

    @property
    def probe_available(self):
        return self.probe_basename is not None

    @property
    def probe_executable(self):
        return self._paths.get(self.probe_basename)

    @staticmethod
    def stream_copy_opts(copy=True, *, ext=None):
        yield from ('-map', '0')
        # Don't copy Apple TV chapters track, bin_data
        # See https://github.com/yt-dlp/yt-dlp/issues/2, #19042, #19024, https://trac.ffmpeg.org/ticket/6016
        yield from ('-dn', '-ignore_unknown')
        if copy:
            yield from ('-c', 'copy')
        if ext in ('mp4', 'mov', 'm4a'):
            yield from ('-c:s', 'mov_text')

    def check_version(self):
        if not self.available:
            raise FFmpegPostProcessorError('ffmpeg not found. Please install or provide the path using --ffmpeg-location')

        required_version = '10-0' if self.basename == 'avconv' else '1.0'
        if is_outdated_version(self._version, required_version):
            self.report_warning(f'Your copy of {self.basename} is outdated, update {self.basename} '
                                f'to version {required_version} or newer if you encounter any errors')

    def get_audio_codec(self, path):
        if not self.probe_available and not self.available:
            raise PostProcessingError('ffprobe and ffmpeg not found. Please install or provide the path using --ffmpeg-location')
        try:
            if self.probe_available:
                cmd = [
                    encodeFilename(self.probe_executable, True),
                    encodeArgument('-show_streams')]
            else:
                cmd = [
                    encodeFilename(self.executable, True),
                    encodeArgument('-i')]
            cmd.append(self._ffmpeg_fn_arg_split(path, True, True))
            self.write_debug(f'{self.basename} command line: {shell_quote(cmd)}')
            stdout, stderr, returncode = Popen.run(
                cmd, text=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if returncode != (0 if self.probe_available else 1):
                return None
        except OSError:
            return None
        output = stdout if self.probe_available else stderr
        if self.probe_available:
            audio_codec = None
            for line in output.split('\n'):
                if line.startswith('codec_name='):
                    audio_codec = line.split('=')[1].strip()
                elif line.strip() == 'codec_type=audio' and audio_codec is not None:
                    return audio_codec
        else:
            # Stream #FILE_INDEX:STREAM_INDEX[STREAM_ID](LANGUAGE): CODEC_TYPE: CODEC_NAME
            mobj = re.search(
                r'Stream\s*#\d+:\d+(?:\[0x[0-9a-f]+\])?(?:\([a-z]{3}\))?:\s*Audio:\s*([0-9a-z]+)',
                output)
            if mobj:
                return mobj.group(1)
        return None

    def get_metadata_object(self, path, opts=[]):
        if self.probe_basename != 'ffprobe':
            if self.probe_available:
                self.report_warning('Only ffprobe is supported for metadata extraction')
            raise PostProcessingError('ffprobe not found. Please install or provide the path using --ffmpeg-location')
        self.check_version()

        cmd = [
            encodeFilename(self.probe_executable, True),
            encodeArgument('-hide_banner'),
            encodeArgument('-show_format'),
            encodeArgument('-show_streams'),
            encodeArgument('-print_format'),
            encodeArgument('json'),
        ]

        cmd += opts
        cmd.append(self._ffmpeg_fn_arg_split(path, True, True))
        self.write_debug(f'ffprobe command line: {shell_quote(cmd)}')
        stdout, _, _ = Popen.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
        return json.loads(stdout)

    def get_stream_number(self, path, keys, value):
        streams = self.get_metadata_object(path)['streams']
        num = next(
            (i for i, stream in enumerate(streams) if traverse_obj(stream, keys, casesense=False) == value),
            None)
        return num, len(streams)

    def _get_real_video_duration(self, filepath, fatal=True):
        try:
            duration = float_or_none(
                traverse_obj(self.get_metadata_object(filepath), ('format', 'duration')))
            if not duration:
                raise PostProcessingError('ffprobe returned empty duration')
            return duration
        except PostProcessingError as e:
            if fatal:
                raise PostProcessingError(f'Unable to determine video duration: {e.msg}')

    def _duration_mismatch(self, d1, d2, tolerance=2):
        if not d1 or not d2:
            return None
        # The duration is often only known to nearest second. So there can be <1sec disparity natually.
        # Further excuse an additional <1sec difference.
        return abs(d1 - d2) > tolerance

    def run_ffmpeg_multiple_files(self, input_paths, out_path, opts, **kwargs):
        return self.real_run_ffmpeg(
            [(path, []) for path in input_paths],
            [(out_path, opts)], **kwargs)

    def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, expected_retcodes=(0,), info_dict=None):
        self.check_version()

        oldest_mtime = min(
            self._downloader.stat(path).st_mtime for path, _ in input_path_opts if path)

        cmd = [encodeFilename(self.executable, True), encodeArgument('-y')]

        use_native_progress = self.use_native_progress
        if use_native_progress:
            cmd.extend(['-progress', 'pipe:1', '-stats_period', '0.1'])
        # avconv does not have repeat option
        if self.basename == 'ffmpeg':
            cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]

        def make_args(file, args, name, number):
            keys = ['_%s%d' % (name, number), '_%s' % name]
            if name == 'o':
                args += ['-movflags', '+faststart']
                if number == 1:
                    keys.append('')
            args += self._configuration_args(self.basename, keys)
            if name == 'i':
                args.append('-i')
            return (
                [encodeArgument(arg) for arg in args]
                + [self._ffmpeg_fn_arg_split(file, True, True)])

        for arg_type, path_opts in (('i', input_path_opts), ('o', output_path_opts)):
            cmd += itertools.chain.from_iterable(
                make_args(path, list(opts), arg_type, i + 1)
                for i, (path, opts) in enumerate(path_opts) if path)

        for fn, _ in output_path_opts:
            self._ydl.ensure_directory(fn)

        self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
        if use_native_progress:
            # this is required because read_ffmpeg_status doesn't care about stderr,
            # and sabotaging reading stderr cause ffmpeg to stuck
            with tempfile.TemporaryFile() as ste:
                p = Popen(cmd, stdout=subprocess.PIPE, stderr=ste, stdin=subprocess.PIPE)
                try:
                    retval = -1
                    retval = self.read_ffmpeg_status(info_dict, p, True)
                finally:
                    self._finish_multiline_status()
                ste.seek(0)
                stderr = ste.read()
        else:
            _, stderr, retval = Popen.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)

        if isinstance(stderr, bytes):
            stderr = stderr.decode('utf-8', 'replace')
        if retval not in variadic(expected_retcodes):
            self.write_debug(stderr)
            raise FFmpegPostProcessorError(stderr.strip().splitlines()[-1])

        for out_path, _ in output_path_opts:
            if out_path:
                self.try_utime(out_path, oldest_mtime, oldest_mtime)
        return stderr

    def run_ffmpeg(self, path, out_path, opts, **kwargs):
        return self.run_ffmpeg_multiple_files([path], out_path, opts, **kwargs)

    @staticmethod
    def _ffmpeg_filename_argument(fn):
        # Always use 'file:' because the filename may contain ':' (ffmpeg
        # interprets that as a protocol) or can start with '-' (-- is broken in
        # ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details)
        # Also leave '-' intact in order not to break streaming to stdout.
        if fn.startswith(('http://', 'https://')):
            return fn
        return 'file:' + fn if fn != '-' else fn

    def _ffmpeg_fn_arg_split(self, fn, encoded=False, for_subproc=False):
        " Same as self.encode_filename_fixed(self._ffmpeg_filename_argument(path), True) but with split_longname "

        def may_encode(text):
            if encoded:
                return encodeFilename(text, for_subproc)
            else:
                return text

        if fn.startswith(('http://', 'https://')):
            return may_encode(fn)

        return may_encode('file:' + split_longname_str(fn) if fn != '-' else fn)

    @staticmethod
    def _quote_for_ffmpeg(string):
        # See https://ffmpeg.org/ffmpeg-utils.html#toc-Quoting-and-escaping
        # A sequence of '' produces '\'''\'';
        # final replace removes the empty '' between \' \'.
        string = string.replace("'", r"'\''").replace("'''", "'")
        # Handle potential ' at string boundaries.
        string = string[1:] if string[0] == "'" else "'" + string
        return string[:-1] if string[-1] == "'" else string + "'"

    def force_keyframes(self, filename, timestamps):
        timestamps = orderedSet(timestamps)
        if timestamps[0] == 0:
            timestamps = timestamps[1:]
        keyframe_file = prepend_extension(filename, 'keyframes.temp')
        self.to_screen(f'Re-encoding "{filename}" with appropriate keyframes')
        self.run_ffmpeg(filename, keyframe_file, [
            *self.stream_copy_opts(False, ext=determine_ext(filename)),
            '-force_key_frames', ','.join(f'{t:.6f}' for t in timestamps)])
        return keyframe_file

    def concat_files(self, in_files, out_file, concat_opts=None):
        """
        Use concat demuxer to concatenate multiple files having identical streams.

        Only inpoint, outpoint, and duration concat options are supported.
        See https://ffmpeg.org/ffmpeg-formats.html#concat-1 for details
        """
        concat_file = f'{out_file}.concat'
        self.write_debug(f'Writing concat spec to {concat_file}')
        with open(concat_file, 'wt', encoding='utf-8') as f:
            f.writelines(self._concat_spec(in_files, concat_opts))

        out_flags = list(self.stream_copy_opts(ext=determine_ext(out_file)))

        self.real_run_ffmpeg(
            [(concat_file, ['-hide_banner', '-nostdin', '-f', 'concat', '-safe', '0'])],
            [(out_file, out_flags)])
        self._delete_downloaded_files(concat_file)

    @classmethod
    def _concat_spec(cls, in_files, concat_opts=None):
        if concat_opts is None:
            concat_opts = [{}] * len(in_files)
        yield 'ffconcat version 1.0\n'
        for file, opts in zip(in_files, concat_opts):
            yield f'file {cls._quote_for_ffmpeg(cls._ffmpeg_filename_argument(file))}\n'
            # Iterate explicitly to yield the following directives in order, ignoring the rest.
            for directive in 'inpoint', 'outpoint', 'duration':
                if directive in opts:
                    yield f'{directive} {opts[directive]}\n'

    def report_progress(self, s):
        if self.use_native_progress:
            if s.get('status') == 'finished' and not s.get('__from_ffmpeg_native_status'):
                return
            ShowsProgress.report_progress(self, s)
        PostProcessor.report_progress(self, s)

class FFmpegExtractAudioPP(FFmpegPostProcessor):
    COMMON_AUDIO_EXTS = MEDIA_EXTENSIONS.common_audio + ('wma', )
    SUPPORTED_EXTS = tuple(ACODECS.keys())
    FORMAT_RE = create_mapping_re(('best', *SUPPORTED_EXTS))

    def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
        FFmpegPostProcessor.__init__(self, downloader)
        self.mapping = preferredcodec or 'best'
        self._preferredquality = float_or_none(preferredquality)
        self._nopostoverwrites = nopostoverwrites

    def _quality_args(self, codec):
        if self._preferredquality is None:
            return []
        elif self._preferredquality > 10:
            return ['-b:a', f'{self._preferredquality}k']

        limits = {
            'libmp3lame': (10, 0),
            'libvorbis': (0, 10),
            # FFmpeg's AAC encoder does not have an upper limit for the value of -q:a.
            # Experimentally, with values over 4, bitrate changes were minimal or non-existent
            'aac': (0.1, 4),
            'libfdk_aac': (1, 5),
        }.get(codec)
        if not limits:
            return []

        q = limits[1] + (limits[0] - limits[1]) * (self._preferredquality / 10)
        if codec == 'libfdk_aac':
            return ['-vbr', f'{int(q)}']
        return ['-q:a', f'{q}']

    def run_ffmpeg(self, path, out_path, codec, more_opts, information=None):
        if codec is None:
            acodec_opts = []
        else:
            acodec_opts = ['-acodec', codec]
        opts = ['-vn'] + acodec_opts + more_opts
        try:
            FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts, info_dict=information)
        except FFmpegPostProcessorError as err:
            raise PostProcessingError(f'audio conversion failed: {err.msg}')

    @PostProcessor._restrict_to(images=False)
    def run(self, information):
        orig_path = path = information['filepath']
        target_format, _skip_msg = resolve_mapping(information['ext'], self.mapping)
        if target_format == 'best' and information['ext'] in self.COMMON_AUDIO_EXTS:
            target_format, _skip_msg = None, 'the file is already in a common audio format'
        if not target_format:
            self.to_screen(f'Not converting audio {orig_path}; {_skip_msg}')
            return [], information

        filecodec = self.get_audio_codec(path)
        if filecodec is None:
            raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe')

        if filecodec == 'aac' and target_format in ('m4a', 'best'):
            # Lossless, but in another container
            extension, _, more_opts, acodec = *ACODECS['m4a'], 'copy'
        elif target_format == 'best' or target_format == filecodec:
            # Lossless if possible
            try:
                extension, _, more_opts, acodec = *ACODECS[filecodec], 'copy'
            except KeyError:
                extension, acodec, more_opts = ACODECS['mp3']
        else:
            # We convert the audio (lossy if codec is lossy)
            extension, acodec, more_opts = ACODECS[target_format]
            if acodec == 'aac' and self._features.get('fdk'):
                acodec, more_opts = 'libfdk_aac', []

        more_opts = list(more_opts)
        if acodec != 'copy':
            more_opts = self._quality_args(acodec)

        # not os.path.splitext, since the latter does not work on unicode in all setups
        temp_path = new_path = f'{path.rpartition(".")[0]}.{extension}'

        if new_path == path:
            if acodec == 'copy':
                self.to_screen(f'Not converting audio {orig_path}; file is already in target format {target_format}')
                return [], information
            orig_path = prepend_extension(path, 'orig')
            temp_path = prepend_extension(path, 'temp')
        if (self._nopostoverwrites and self._downloader.exists(encodeFilename(new_path))
                and self._downloader.exists(encodeFilename(orig_path))):
            self.to_screen('Post-process file %s exists, skipping' % new_path)
            return [], information

        self.to_screen(f'Destination: {new_path}')
        self.run_ffmpeg(path, temp_path, acodec, more_opts, information)

        os.replace(path, orig_path)
        os.replace(temp_path, new_path)
        information['filepath'] = new_path
        information['ext'] = extension

        # Try to update the date time for extracted audio file.
        if information.get('filetime') is not None:
            self.try_utime(
                new_path, time.time(), information['filetime'], errnote='Cannot update utime of audio file')

        return [orig_path], information

class FFmpegVideoConvertorPP(FFmpegPostProcessor):
    SUPPORTED_EXTS = (*MEDIA_EXTENSIONS.common_video, *sorted(MEDIA_EXTENSIONS.common_audio + ('aac', 'vorbis')))
    FORMAT_RE = create_mapping_re(SUPPORTED_EXTS)
    _ACTION = 'converting'

    def __init__(self, downloader=None, preferedformat=None):
        super().__init__(downloader)
        self.mapping = preferedformat

    @staticmethod
    def _options(target_ext):
        yield from FFmpegPostProcessor.stream_copy_opts(False)
        if target_ext == 'avi':
            yield from ('-c:v', 'libxvid', '-vtag', 'XVID')

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        filename, source_ext = info['filepath'], info['ext'].lower()
        target_ext, _skip_msg = resolve_mapping(source_ext, self.mapping)
        if _skip_msg:
            self.to_screen(f'Not {self._ACTION} media file "{filename}"; {_skip_msg}')
            return [], info

        outpath = replace_extension(filename, target_ext, source_ext)
        self.to_screen(f'{self._ACTION.title()} video from {source_ext} to {target_ext}; Destination: {outpath}')
        self.run_ffmpeg(filename, outpath, self._options(target_ext), info_dict=info)

        info['filepath'] = outpath
        info['format'] = info['ext'] = target_ext
        return [filename], info

class FFmpegVideoRemuxerPP(FFmpegVideoConvertorPP):
    _ACTION = 'remuxing'
    _NATIVE_PROGRESS_ENABLED = True

    @staticmethod
    def _options(target_ext):
        return FFmpegPostProcessor.stream_copy_opts()

class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
    _NATIVE_PROGRESS_ENABLED = True
    SUPPORTED_EXTS = ('mp4', 'mov', 'm4a', 'webm', 'mkv', 'mka')

    def __init__(self, downloader=None, already_have_subtitle=False):
        super().__init__(downloader)
        self._already_have_subtitle = already_have_subtitle

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        if info['ext'] not in self.SUPPORTED_EXTS:
            self.to_screen(f'Subtitles can only be embedded in {", ".join(self.SUPPORTED_EXTS)} files')
            return [], info
        subtitles = info.get('requested_subtitles')
        if not subtitles:
            self.to_screen('There aren\'t any subtitles to embed')
            return [], info

        filename = info['filepath']

        # Disabled temporarily. There needs to be a way to override this
        # in case of duration actually mismatching in extractor
        # See: https://github.com/yt-dlp/yt-dlp/issues/1870, https://github.com/yt-dlp/yt-dlp/issues/1385
        '''
        if info.get('duration') and not info.get('__real_download') and self._duration_mismatch(
                self._get_real_video_duration(filename, False), info['duration']):
            self.to_screen(f'Skipping {self.pp_key()} since the real and expected durations mismatch')
            return [], info
        '''

        ext = info['ext']
        sub_langs, sub_names, sub_filenames = [], [], []
        webm_vtt_warn = False
        mp4_ass_warn = False

        for lang, sub_info in subtitles.items():
            if not os.path.exists(sub_info.get('filepath', '')):
                self.report_warning(f'Skipping embedding {lang} subtitle because the file is missing')
                continue
            sub_ext = sub_info['ext']
            if sub_ext in ('json', 'xml'):
                self.report_warning(f'{sub_ext.upper()} subtitles cannot be embedded')
            elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt':
                sub_langs.append(lang)
                sub_names.append(sub_info.get('name'))
                sub_filenames.append(sub_info['filepath'])
            else:
                if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt':
                    webm_vtt_warn = True
                    self.report_warning('Only WebVTT subtitles can be embedded in webm files')
            if not mp4_ass_warn and ext == 'mp4' and sub_ext == 'ass':
                mp4_ass_warn = True
                self.report_warning('ASS subtitles cannot be properly embedded in mp4 files; expect issues')

        if not sub_langs:
            return [], info

        input_files = [filename] + sub_filenames

        opts = [
            *self.stream_copy_opts(ext=info['ext']),
            # Don't copy the existing subtitles, we may be running the
            # postprocessor a second time
            '-map', '-0:s',
        ]
        for i, (lang, name) in enumerate(zip(sub_langs, sub_names)):
            opts.extend(['-map', '%d:0' % (i + 1)])
            lang_code = ISO639Utils.short2long(lang) or lang
            opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
            if name:
                opts.extend(['-metadata:s:s:%d' % i, 'handler_name=%s' % name,
                             '-metadata:s:s:%d' % i, 'title=%s' % name])

        temp_filename = prepend_extension(filename, 'temp')
        self.to_screen('Embedding subtitles in "%s"' % filename)
        self.run_ffmpeg_multiple_files(input_files, temp_filename, opts, info_dict=info)
        self._downloader.replace(temp_filename, filename)

        files_to_delete = [] if self._already_have_subtitle else sub_filenames
        return files_to_delete, info

class FFmpegMetadataPP(FFmpegPostProcessor):
    _NATIVE_PROGRESS_ENABLED = True

    def __init__(self, downloader, add_metadata=True, add_chapters=True, add_infojson='if_exists'):
        FFmpegPostProcessor.__init__(self, downloader)
        self._add_metadata = add_metadata
        self._add_chapters = add_chapters
        self._add_infojson = add_infojson

    @staticmethod
    def _options(target_ext):
        audio_only = target_ext == 'm4a'
        yield from FFmpegPostProcessor.stream_copy_opts(not audio_only)
        if audio_only:
            yield from ('-vn', '-acodec', 'copy')

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        filename, metadata_filename = info['filepath'], None
        files_to_delete, options = [], []
        if self._add_chapters and info.get('chapters'):
            metadata_filename = replace_extension(filename, 'meta')
            options.extend(self._get_chapter_opts(info['chapters'], metadata_filename))
            files_to_delete.append(metadata_filename)
        if self._add_metadata:
            options.extend(self._get_metadata_opts(info))

        if self._add_infojson:
            if info['ext'] in ('mkv', 'mka'):
                infojson_filename = info.get('infojson_filename')
                options.extend(self._get_infojson_opts(info, infojson_filename))
                if not infojson_filename:
                    files_to_delete.append(info.get('infojson_filename'))
            elif self._add_infojson is True:
                self.to_screen('The info-json can only be attached to mkv/mka files')

        if not options:
            self.to_screen('There isn\'t any metadata to add')
            return [], info

        temp_filename = prepend_extension(filename, 'temp')
        self.to_screen('Adding metadata to "%s"' % filename)
        self.run_ffmpeg_multiple_files(
            (filename, metadata_filename), temp_filename,
            itertools.chain(self._options(info['ext']), *options),
            info_dict=info)
        self._delete_downloaded_files(*files_to_delete)
        os.replace(temp_filename, filename)
        return [], info

    @staticmethod
    def _get_chapter_opts(chapters, metadata_filename):
        with open(metadata_filename, 'wt', encoding='utf-8') as f:
            def ffmpeg_escape(text):
                return re.sub(r'([\\=;#\n])', r'\\\1', text)

            metadata_file_content = ';FFMETADATA1\n'
            for chapter in chapters:
                metadata_file_content += '[CHAPTER]\nTIMEBASE=1/1000\n'
                metadata_file_content += 'START=%d\n' % (chapter['start_time'] * 1000)
                metadata_file_content += 'END=%d\n' % (chapter['end_time'] * 1000)
                chapter_title = chapter.get('title')
                if chapter_title:
                    metadata_file_content += 'title=%s\n' % ffmpeg_escape(chapter_title)
            f.write(metadata_file_content)
        yield ('-map_metadata', '1')

    def _get_metadata_opts(self, info):
        meta_prefix = 'meta'
        metadata = collections.defaultdict(dict)

        def add(meta_list, info_list=None):
            value = next((
                str(info[key]) for key in [f'{meta_prefix}_'] + list(variadic(info_list or meta_list))
                if info.get(key) is not None), None)
            if value not in ('', None):
                value = value.replace('\0', '')  # nul character cannot be passed in command line
                metadata['common'].update({meta_f: value for meta_f in variadic(meta_list)})

        # Info on media metadata/metadata supported by ffmpeg:
        # https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
        # https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/
        # https://kodi.wiki/view/Video_file_tagging

        add('title', ('track', 'title'))
        add('date', 'upload_date')
        add(('description', 'synopsis'), 'description')
        add(('purl', 'comment'), 'webpage_url')
        add('track', 'track_number')
        add('artist', ('artist', 'creator', 'uploader', 'uploader_id'))
        add('genre')
        add('album')
        add('album_artist')
        add('disc', 'disc_number')
        add('show', 'series')
        add('season_number')
        add('episode_id', ('episode', 'episode_id'))
        add('episode_sort', 'episode_number')
        if 'embed-metadata' in self.get_param('compat_opts', []):
            add('comment', 'description')
            metadata['common'].pop('synopsis', None)

        meta_regex = rf'{re.escape(meta_prefix)}(?P<i>\d+)?_(?P<key>.+)'
        for key, value in info.items():
            mobj = re.fullmatch(meta_regex, key)
            if value is not None and mobj:
                metadata[mobj.group('i') or 'common'][mobj.group('key')] = value.replace('\0', '')

        # Write id3v1 metadata also since Windows Explorer can't handle id3v2 tags
        yield ('-write_id3v1', '1')

        for name, value in metadata['common'].items():
            yield ('-metadata', f'{name}={value}')

        stream_idx = 0
        for fmt in info.get('requested_formats') or []:
            stream_count = 2 if 'none' not in (fmt.get('vcodec'), fmt.get('acodec')) else 1
            lang = ISO639Utils.short2long(fmt.get('language') or '') or fmt.get('language')
            for i in range(stream_idx, stream_idx + stream_count):
                if lang:
                    metadata[str(i)].setdefault('language', lang)
                for name, value in metadata[str(i)].items():
                    yield (f'-metadata:s:{i}', f'{name}={value}')
            stream_idx += stream_count

    def _get_infojson_opts(self, info, infofn):
        if not infofn or not os.path.exists(infofn):
            if self._add_infojson is not True:
                return
            infofn = infofn or '%s.temp' % (
                self._downloader.prepare_filename(info, 'infojson')
                or replace_extension(self._downloader.prepare_filename(info), 'info.json', info['ext']))
            if not self._downloader._ensure_dir_exists(infofn):
                return
            self.write_debug(f'Writing info-json to: {infofn}')
            write_json_file(self._downloader.sanitize_info(info, self.get_param('clean_infojson', True)), infofn)
            info['infojson_filename'] = infofn

        old_stream, new_stream = self.get_stream_number(info['filepath'], ('tags', 'mimetype'), 'application/json')
        if old_stream is not None:
            yield ('-map', '-0:%d' % old_stream)
            new_stream -= 1

        yield (
            '-attach', infofn,
            f'-metadata:s:{new_stream}', 'mimetype=application/json',
            f'-metadata:s:{new_stream}', 'filename=info.json',
        )

class FFmpegMergerPP(FFmpegPostProcessor):
    _NATIVE_PROGRESS_ENABLED = True
    SUPPORTED_EXTS = MEDIA_EXTENSIONS.common_video

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        filename = info['filepath']
        temp_filename = prepend_extension(filename, 'temp')
        args = ['-c', 'copy']
        audio_streams = 0
        for (i, fmt) in enumerate(info['requested_formats']):
            if fmt.get('acodec') != 'none':
                args.extend(['-map', f'{i}:a:0'])
                aac_fixup = fmt['protocol'].startswith('m3u8') and self.get_audio_codec(fmt['filepath']) == 'aac'
                if aac_fixup:
                    args.extend([f'-bsf:a:{audio_streams}', 'aac_adtstoasc'])
                audio_streams += 1
            if fmt.get('vcodec') != 'none':
                args.extend(['-map', '%u:v:0' % (i)])
        self.to_screen('Merging formats into "%s"' % filename)
        self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args, info_dict=info)
        self._downloader.rename(temp_filename, filename)
        return info['__files_to_merge'], info

    def can_merge(self):
        # TODO: figure out merge-capable ffmpeg version
        if self.basename != 'avconv':
            return True

        required_version = '10-0'
        if is_outdated_version(
                self._versions[self.basename], required_version):
            warning = ('Your copy of %s is outdated and unable to properly mux separate video and audio files, '
                       'yt-dlp will download single file media. '
                       'Update %s to version %s or newer to fix this.') % (
                           self.basename, self.basename, required_version)
            self.report_warning(warning)
            return False
        return True

class FFmpegFixupPostProcessor(FFmpegPostProcessor):
    _NATIVE_PROGRESS_ENABLED = True

    def _fixup(self, msg, filename, options, info_dict=None):
        temp_filename = prepend_extension(filename, 'temp')

        self.to_screen(f'{msg} of "{filename}"')
        self.run_ffmpeg(filename, temp_filename, options, info_dict=info_dict)

        self._downloader.replace(temp_filename, filename)

class FFmpegFixupStretchedPP(FFmpegFixupPostProcessor):
    @PostProcessor._restrict_to(images=False, audio=False)
    def run(self, info):
        stretched_ratio = info.get('stretched_ratio')
        if stretched_ratio not in (None, 1):
            self._fixup('Fixing aspect ratio', info['filepath'], [
                *self.stream_copy_opts(), '-aspect', '%f' % stretched_ratio],
                info_dict=info)
        return [], info

class FFmpegFixupM4aPP(FFmpegFixupPostProcessor):
    @PostProcessor._restrict_to(images=False, video=False)
    def run(self, info):
        if info.get('container') == 'm4a_dash':
            self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(), '-f', 'mp4'], info_dict=info)
        return [], info

class FFmpegFixupM3u8PP(FFmpegFixupPostProcessor):
    def _needs_fixup(self, info):
        yield info['ext'] in ('mp4', 'm4a')
        yield info['protocol'].startswith('m3u8')
        try:
            metadata = self.get_metadata_object(info['filepath'])
        except PostProcessingError as e:
            self.report_warning(f'Unable to extract metadata: {e.msg}')
            yield True
        else:
            yield traverse_obj(metadata, ('format', 'format_name'), casesense=False) == 'mpegts'

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        if all(self._needs_fixup(info)):
            self._fixup('Fixing MPEG-TS in MP4 container', info['filepath'], [
                *self.stream_copy_opts(), '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'],
                info_dict=info)
        return [], info

class FFmpegFixupTimestampPP(FFmpegFixupPostProcessor):

    def __init__(self, downloader=None, trim=0.001):
        # "trim" should be used when the video contains unintended packets
        super().__init__(downloader)
        assert isinstance(trim, (int, float))
        self.trim = str(trim)

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        if not self._features.get('setts'):
            self.report_warning(
                'A re-encode is needed to fix timestamps in older versions of ffmpeg. '
                'Please install ffmpeg 4.4 or later to fixup without re-encoding')
            opts = ['-vf', 'setpts=PTS-STARTPTS']
        else:
            opts = ['-c', 'copy', '-bsf', 'setts=ts=TS-STARTPTS']
        self._fixup('Fixing frame timestamp', info['filepath'], opts + [*self.stream_copy_opts(False), '-ss', self.trim], info_dict=info)
        return [], info

class FFmpegCopyStreamPP(FFmpegFixupPostProcessor):
    MESSAGE = 'Copying stream'

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts(), info_dict=info)
        return [], info

class FFmpegFixupDurationPP(FFmpegCopyStreamPP):
    MESSAGE = 'Fixing video duration'

class FFmpegFixupDuplicateMoovPP(FFmpegCopyStreamPP):
    MESSAGE = 'Fixing duplicate MOOV atoms'

class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
    SUPPORTED_EXTS = MEDIA_EXTENSIONS.subtitles

    def __init__(self, downloader=None, format=None):
        super().__init__(downloader)
        self.format = format

    def run(self, info):
        subs = info.get('requested_subtitles')
        new_ext = self.format
        new_format = new_ext
        if new_format == 'vtt':
            new_format = 'webvtt'
        if subs is None:
            self.to_screen('There aren\'t any subtitles to convert')
            return [], info
        self.to_screen('Converting subtitles')
        sub_filenames = []
        for lang, sub in subs.items():
            if not os.path.exists(sub.get('filepath', '')):
                self.report_warning(f'Skipping embedding {lang} subtitle because the file is missing')
                continue
            ext = sub['ext']
            if ext == new_ext:
                self.to_screen('Subtitle file for %s is already in the requested format' % new_ext)
                continue
            elif ext == 'json':
                self.to_screen(
                    'You have requested to convert json subtitles into another format, '
                    'which is currently not possible')
                continue
            old_file = sub['filepath']
            sub_filenames.append(old_file)
            new_file = replace_extension(old_file, new_ext)

            if ext in ('dfxp', 'ttml', 'tt'):
                self.report_warning(
                    'You have requested to convert dfxp (TTML) subtitles into another format, '
                    'which results in style information loss')

                dfxp_file = old_file
                srt_file = replace_extension(old_file, 'srt')

                with open(dfxp_file, 'rb') as f:
                    srt_data = dfxp2srt(f.read())

                with open(srt_file, 'wt', encoding='utf-8') as f:
                    f.write(srt_data)
                old_file = srt_file

                subs[lang] = {
                    'ext': 'srt',
                    'data': srt_data,
                    'filepath': srt_file,
                }

                if new_ext == 'srt':
                    continue
                else:
                    sub_filenames.append(srt_file)

            self.run_ffmpeg(old_file, new_file, ['-f', new_format])

            with open(new_file, encoding='utf-8') as f:
                subs[lang] = {
                    'ext': new_ext,
                    'data': f.read(),
                    'filepath': new_file,
                }

            info['__files_to_move'][new_file] = replace_extension(
                info['__files_to_move'][sub['filepath']], new_ext)

        return sub_filenames, info

class FFmpegSplitChaptersPP(FFmpegPostProcessor):
    def __init__(self, downloader, force_keyframes=False):
        FFmpegPostProcessor.__init__(self, downloader)
        self._force_keyframes = force_keyframes

    def _prepare_filename(self, number, chapter, info):
        info = info.copy()
        info.update({
            'section_number': number,
            'section_title': chapter.get('title'),
            'section_start': chapter.get('start_time'),
            'section_end': chapter.get('end_time'),
        })
        return self._downloader.prepare_filename(info, 'chapter')

    def _ffmpeg_args_for_chapter(self, number, chapter, info):
        destination = self._prepare_filename(number, chapter, info)
        if not self._downloader._ensure_dir_exists(encodeFilename(destination)):
            return

        chapter['filepath'] = destination
        self.to_screen('Chapter %03d; Destination: %s' % (number, destination))
        return (
            destination,
            ['-ss', str(chapter['start_time']),
             '-t', str(chapter['end_time'] - chapter['start_time'])])

    @PostProcessor._restrict_to(images=False)
    def run(self, info):
        chapters = info.get('chapters') or []
        if not chapters:
            self.to_screen('Chapter information is unavailable')
            return [], info

        in_file = info['filepath']
        if self._force_keyframes and len(chapters) > 1:
            in_file = self.force_keyframes(in_file, (c['start_time'] for c in chapters))
        self.to_screen('Splitting video by chapters; %d chapters found' % len(chapters))
        for idx, chapter in enumerate(chapters):
            destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
            self.real_run_ffmpeg([(in_file, opts)], [(destination, self.stream_copy_opts())])
        if in_file != info['filepath']:
            self._delete_downloaded_files(in_file, msg=None)
        return [], info

class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor):
    SUPPORTED_EXTS = MEDIA_EXTENSIONS.thumbnails
    FORMAT_RE = create_mapping_re(SUPPORTED_EXTS)

    def __init__(self, downloader=None, format=None):
        super().__init__(downloader)
        self.mapping = format

    @classmethod
    def is_webp(cls, path):
        deprecation_warning(f'{cls.__module__}.{cls.__name__}.is_webp is deprecated')
        return imghdr.what(path) == 'webp'

    def fixup_webp(self, info, idx=-1):
        thumbnail_filename = info['thumbnails'][idx]['filepath']
        _, thumbnail_ext = os.path.splitext(thumbnail_filename)
        if thumbnail_ext:
            if thumbnail_ext.lower() != '.webp' and imghdr.what(thumbnail_filename) == 'webp':
                self.to_screen('Correcting thumbnail "%s" extension to webp' % thumbnail_filename)
                webp_filename = replace_extension(thumbnail_filename, 'webp')
                self._downloader.replace(thumbnail_filename, webp_filename)
                info['thumbnails'][idx]['filepath'] = webp_filename
                info['__files_to_move'][webp_filename] = replace_extension(
                    info['__files_to_move'].pop(thumbnail_filename), 'webp')

    @staticmethod
    def _options(target_ext):
        if target_ext == 'jpg':
            return ['-bsf:v', 'mjpeg2jpeg']
        return []

    def convert_thumbnail(self, thumbnail_filename, target_ext):
        thumbnail_conv_filename = replace_extension(thumbnail_filename, target_ext)

        self.to_screen(f'Converting thumbnail "{thumbnail_filename}" to {target_ext}')
        _, source_ext = os.path.splitext(thumbnail_filename)
        self.real_run_ffmpeg(
            [(thumbnail_filename, [] if source_ext == '.gif' else ['-f', 'image2', '-pattern_type', 'none'])],
            [(thumbnail_conv_filename.replace('%', '%%'), self._options(target_ext))])
        return thumbnail_conv_filename

    def run(self, info):
        files_to_delete = []
        has_thumbnail = False

        for idx, thumbnail_dict in enumerate(info.get('thumbnails') or []):
            original_thumbnail = thumbnail_dict.get('filepath')
            if not original_thumbnail:
                continue
            has_thumbnail = True
            self.fixup_webp(info, idx)
            original_thumbnail = thumbnail_dict['filepath']  # Path can change during fixup
            thumbnail_ext = os.path.splitext(original_thumbnail)[1][1:].lower()
            if thumbnail_ext == 'jpeg':
                thumbnail_ext = 'jpg'
            target_ext, _skip_msg = resolve_mapping(thumbnail_ext, self.mapping)
            if _skip_msg:
                self.to_screen(f'Not converting thumbnail "{original_thumbnail}"; {_skip_msg}')
                continue
            thumbnail_dict['filepath'] = self.convert_thumbnail(original_thumbnail, target_ext)
            files_to_delete.append(original_thumbnail)
            info['__files_to_move'][thumbnail_dict['filepath']] = replace_extension(
                info['__files_to_move'][original_thumbnail], target_ext)

        if not has_thumbnail:
            self.to_screen('There aren\'t any thumbnails to convert')
        return files_to_delete, info

class FFmpegConcatPP(FFmpegPostProcessor):
    def __init__(self, downloader, only_multi_video=False):
        self._only_multi_video = only_multi_video
        super().__init__(downloader)

    def _get_codecs(self, file):
        codecs = traverse_obj(self.get_metadata_object(file), ('streams', ..., 'codec_name'))
        self.write_debug(f'Codecs = {", ".join(codecs)}')
        return tuple(codecs)

    def concat_files(self, in_files, out_file):
        if not self._downloader._ensure_dir_exists(out_file):
            return
        if len(in_files) == 1:
            if os.path.realpath(in_files[0]) != os.path.realpath(out_file):
                self.to_screen(f'Moving "{in_files[0]}" to "{out_file}"')
            os.replace(in_files[0], out_file)
            return []

        if len(set(map(self._get_codecs, in_files))) > 1:
            raise PostProcessingError(
                'The files have different streams/codecs and cannot be concatenated. '
                'Either select different formats or --recode-video them to a common format')

        self.to_screen(f'Concatenating {len(in_files)} files; Destination: {out_file}')
        super().concat_files(in_files, out_file)
        return in_files

    @PostProcessor._restrict_to(images=False, simulated=False)
    def run(self, info):
        entries = info.get('entries') or []
        if not any(entries) or (self._only_multi_video and info['_type'] != 'multi_video'):
            return [], info
        elif traverse_obj(entries, (..., lambda k, v: k == 'requested_downloads' and len(v) > 1)):
            raise PostProcessingError('Concatenation is not supported when downloading multiple separate formats')

        in_files = traverse_obj(entries, (..., 'requested_downloads', 0, 'filepath')) or []
        if len(in_files) < len(entries):
            raise PostProcessingError('Aborting concatenation because some downloads failed')

        exts = traverse_obj(entries, (..., 'requested_downloads', 0, 'ext'), (..., 'ext'))
        ie_copy = collections.ChainMap({'ext': exts[0] if len(set(exts)) == 1 else 'mkv'},
                                       info, self._downloader._playlist_infodict(info))
        out_file = self._downloader.prepare_filename(ie_copy, 'pl_video')

        files_to_delete = self.concat_files(in_files, out_file)

        info['requested_downloads'] = [{
            'filepath': out_file,
            'ext': ie_copy['ext'],
        }]
        return files_to_delete, info