Upgrade yt_dlp and download script
This commit is contained in:
@@ -33,15 +33,38 @@ from .movefilesafterdownload import MoveFilesAfterDownloadPP
|
||||
from .sponskrub import SponSkrubPP
|
||||
from .sponsorblock import SponsorBlockPP
|
||||
from .xattrpp import XAttrMetadataPP
|
||||
from ..plugins import load_plugins
|
||||
from ..globals import plugin_pps, postprocessors
|
||||
from ..plugins import PACKAGE_NAME, register_plugin_spec, PluginSpec
|
||||
from ..utils import deprecation_warning
|
||||
|
||||
_PLUGIN_CLASSES = load_plugins('postprocessor', 'PP')
|
||||
|
||||
def __getattr__(name):
|
||||
lookup = plugin_pps.value
|
||||
if name in lookup:
|
||||
deprecation_warning(
|
||||
f'Importing a plugin Post-Processor from {__name__} is deprecated. '
|
||||
f'Please import {PACKAGE_NAME}.postprocessor.{name} instead.')
|
||||
return lookup[name]
|
||||
|
||||
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
||||
|
||||
|
||||
def get_postprocessor(key):
|
||||
return globals()[key + 'PP']
|
||||
return postprocessors.value[key + 'PP']
|
||||
|
||||
|
||||
globals().update(_PLUGIN_CLASSES)
|
||||
__all__ = [name for name in globals().keys() if name.endswith('PP')]
|
||||
__all__.extend(('PostProcessor', 'FFmpegPostProcessor'))
|
||||
register_plugin_spec(PluginSpec(
|
||||
module_name='postprocessor',
|
||||
suffix='PP',
|
||||
destination=postprocessors,
|
||||
plugin_destination=plugin_pps,
|
||||
))
|
||||
|
||||
_default_pps = {
|
||||
name: value
|
||||
for name, value in globals().items()
|
||||
if name.endswith('PP') or name in ('FFmpegPostProcessor', 'PostProcessor')
|
||||
}
|
||||
postprocessors.value.update(_default_pps)
|
||||
|
||||
__all__ = list(_default_pps.values())
|
||||
|
@@ -9,8 +9,8 @@ from ..utils import (
|
||||
RetryManager,
|
||||
_configuration_args,
|
||||
deprecation_warning,
|
||||
encodeFilename,
|
||||
)
|
||||
from ..utils._utils import _ProgressState
|
||||
|
||||
|
||||
class PostProcessorMetaClass(type):
|
||||
@@ -65,7 +65,7 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||
|
||||
def to_screen(self, text, prefix=True, *args, **kwargs):
|
||||
if self._downloader:
|
||||
tag = '[%s] ' % self.PP_NAME if prefix else ''
|
||||
tag = f'[{self.PP_NAME}] ' if prefix else ''
|
||||
return self._downloader.to_screen(f'{tag}{text}', *args, **kwargs)
|
||||
|
||||
def report_warning(self, text, *args, **kwargs):
|
||||
@@ -127,7 +127,7 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||
if allowed[format_type]:
|
||||
return func(self, info)
|
||||
else:
|
||||
self.to_screen('Skipping %s' % format_type)
|
||||
self.to_screen(f'Skipping {format_type}')
|
||||
return [], info
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -151,7 +151,7 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||
|
||||
def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
|
||||
try:
|
||||
os.utime(encodeFilename(path), (atime, mtime))
|
||||
os.utime(path, (atime, mtime))
|
||||
except Exception:
|
||||
self.report_warning(errnote)
|
||||
|
||||
@@ -174,7 +174,7 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||
self._progress_hooks.append(ph)
|
||||
|
||||
def report_progress(self, s):
|
||||
s['_default_template'] = '%(postprocessor)s %(status)s' % s
|
||||
s['_default_template'] = '%(postprocessor)s %(status)s' % s # noqa: UP031
|
||||
if not self._downloader:
|
||||
return
|
||||
|
||||
@@ -190,7 +190,7 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||
|
||||
self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
|
||||
progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
|
||||
progress_dict))
|
||||
progress_dict), _ProgressState.from_dict(s), s.get('_percent'))
|
||||
|
||||
def _retry_download(self, err, count, retries):
|
||||
# While this is not an extractor, it behaves similar to one and
|
||||
|
@@ -12,8 +12,6 @@ from ..utils import (
|
||||
PostProcessingError,
|
||||
check_executable,
|
||||
encodeArgument,
|
||||
encodeFilename,
|
||||
error_to_compat_str,
|
||||
prepend_extension,
|
||||
shell_quote,
|
||||
)
|
||||
@@ -48,7 +46,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
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))
|
||||
self.report_warning(f'unable to find the thumbnail resolution; {err}')
|
||||
return guess()
|
||||
return int(mobj.group('w')), int(mobj.group('h'))
|
||||
|
||||
@@ -69,7 +67,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
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)):
|
||||
if not os.path.exists(thumbnail_filename):
|
||||
self.report_warning('Skipping embedding the thumbnail because the file is missing.')
|
||||
return [], info
|
||||
|
||||
@@ -86,7 +84,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
thumbnail_filename = convertor.convert_thumbnail(thumbnail_filename, 'png')
|
||||
thumbnail_ext = 'png'
|
||||
|
||||
mtime = os.stat(encodeFilename(filename)).st_mtime
|
||||
mtime = os.stat(filename).st_mtime
|
||||
|
||||
success = True
|
||||
if info['ext'] == 'mp3':
|
||||
@@ -104,12 +102,12 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
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])
|
||||
options.extend(['-map', f'-0:{old_stream}'])
|
||||
new_stream -= 1
|
||||
options.extend([
|
||||
'-attach', self._ffmpeg_filename_argument(thumbnail_filename),
|
||||
'-metadata:s:%d' % new_stream, 'mimetype=%s' % mimetype,
|
||||
'-metadata:s:%d' % new_stream, 'filename=cover.%s' % thumbnail_ext])
|
||||
f'-metadata:s:{new_stream}', f'mimetype={mimetype}',
|
||||
f'-metadata:s:{new_stream}', f'filename=cover.{thumbnail_ext}'])
|
||||
|
||||
self._report_run('ffmpeg', filename)
|
||||
self.run_ffmpeg(filename, temp_filename, options)
|
||||
@@ -120,19 +118,26 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
if not mutagen or prefer_atomicparsley:
|
||||
success = False
|
||||
else:
|
||||
self._report_run('mutagen', filename)
|
||||
f = {'jpeg': MP4Cover.FORMAT_JPEG, 'png': MP4Cover.FORMAT_PNG}
|
||||
try:
|
||||
self._report_run('mutagen', filename)
|
||||
with open(thumbnail_filename, 'rb') as thumbfile:
|
||||
thumb_data = thumbfile.read()
|
||||
|
||||
type_ = imghdr.what(h=thumb_data)
|
||||
if not type_:
|
||||
raise ValueError('could not determine image type')
|
||||
elif type_ not in f:
|
||||
raise ValueError(f'incompatible image type: {type_}')
|
||||
|
||||
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.tags['covr'] = [MP4Cover(data=thumb_data, imageformat=f[type_])]
|
||||
meta.save()
|
||||
temp_filename = filename
|
||||
except Exception as err:
|
||||
self.report_warning('unable to embed using mutagen; %s' % error_to_compat_str(err))
|
||||
self.report_warning(f'unable to embed using mutagen; {err}')
|
||||
success = False
|
||||
|
||||
# Method 2: Use AtomicParsley
|
||||
@@ -148,22 +153,23 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
else:
|
||||
if not prefer_atomicparsley:
|
||||
self.to_screen('mutagen was not found. Falling back to AtomicParsley')
|
||||
cmd = [encodeFilename(atomicparsley, True),
|
||||
encodeFilename(filename, True),
|
||||
cmd = [atomicparsley,
|
||||
filename,
|
||||
encodeArgument('--artwork'),
|
||||
encodeFilename(thumbnail_filename, True),
|
||||
thumbnail_filename,
|
||||
encodeArgument('-o'),
|
||||
encodeFilename(temp_filename, True)]
|
||||
temp_filename]
|
||||
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))
|
||||
self.write_debug(f'AtomicParsley command line: {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()}')
|
||||
success = False
|
||||
# for formats that don't support thumbnails (like 3gp) AtomicParsley
|
||||
# won't create to the temporary file
|
||||
if 'No changes' in stdout:
|
||||
elif 'No changes' in stdout:
|
||||
self.report_warning('The file format doesn\'t support embedding a thumbnail')
|
||||
success = False
|
||||
|
||||
@@ -178,9 +184,9 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
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])
|
||||
options.extend(['-map', f'-0:{old_stream}'])
|
||||
new_stream -= 1
|
||||
options.extend(['-disposition:%s' % new_stream, 'attached_pic'])
|
||||
options.extend([f'-disposition:{new_stream}', 'attached_pic'])
|
||||
|
||||
self._report_run('ffmpeg', filename)
|
||||
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
|
||||
@@ -190,13 +196,13 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
|
||||
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`')
|
||||
raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python3 -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)
|
||||
pic.mime = f'image/{imghdr.what(thumbnail_filename)}'
|
||||
with open(thumbnail_filename, 'rb') as thumbfile:
|
||||
pic.data = thumbfile.read()
|
||||
pic.type = 3 # front cover
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import subprocess
|
||||
|
||||
from .common import PostProcessor
|
||||
from ..compat import compat_shlex_quote
|
||||
from ..utils import PostProcessingError, encodeArgument, variadic
|
||||
from ..utils import Popen, PostProcessingError, shell_quote, variadic
|
||||
|
||||
|
||||
class ExecPP(PostProcessor):
|
||||
@@ -21,16 +18,16 @@ class ExecPP(PostProcessor):
|
||||
if filepath:
|
||||
if '{}' not in cmd:
|
||||
cmd += ' {}'
|
||||
cmd = cmd.replace('{}', compat_shlex_quote(filepath))
|
||||
cmd = cmd.replace('{}', shell_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)
|
||||
self.to_screen(f'Executing command: {cmd}')
|
||||
_, _, return_code = Popen.run(cmd, shell=True)
|
||||
if return_code != 0:
|
||||
raise PostProcessingError(f'Command returned error code {return_code}')
|
||||
return [], info
|
||||
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import collections
|
||||
import contextvars
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
@@ -8,7 +9,7 @@ import subprocess
|
||||
import time
|
||||
|
||||
from .common import PostProcessor
|
||||
from ..compat import functools, imghdr
|
||||
from ..compat import imghdr
|
||||
from ..utils import (
|
||||
MEDIA_EXTENSIONS,
|
||||
ISO639Utils,
|
||||
@@ -20,7 +21,6 @@ from ..utils import (
|
||||
determine_ext,
|
||||
dfxp2srt,
|
||||
encodeArgument,
|
||||
encodeFilename,
|
||||
filter_dict,
|
||||
float_or_none,
|
||||
is_outdated_version,
|
||||
@@ -61,7 +61,7 @@ ACODECS = {
|
||||
|
||||
|
||||
def create_mapping_re(supported):
|
||||
return re.compile(r'{0}(?:/{0})*$'.format(r'(?:\s*\w+\s*>)?\s*(?:%s)\s*' % '|'.join(supported)))
|
||||
return re.compile(r'{0}(?:/{0})*$'.format(r'(?:\s*\w+\s*>)?\s*(?:{})\s*'.format('|'.join(supported))))
|
||||
|
||||
|
||||
def resolve_mapping(source, mapping):
|
||||
@@ -119,7 +119,7 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
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():
|
||||
if basename in self._ffmpeg_to_avconv:
|
||||
self._prefer_ffmpeg = True
|
||||
|
||||
paths = {p: os.path.join(dirname, p) for p in programs}
|
||||
@@ -169,12 +169,12 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
|
||||
@functools.cached_property
|
||||
def basename(self):
|
||||
self._version # run property
|
||||
_ = self._version # run property
|
||||
return self.basename
|
||||
|
||||
@functools.cached_property
|
||||
def probe_basename(self):
|
||||
self._probe_version # run property
|
||||
_ = self._probe_version # run property
|
||||
return self.probe_basename
|
||||
|
||||
def _get_version(self, kind):
|
||||
@@ -202,7 +202,7 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self.basename is not None
|
||||
return bool(self._ffmpeg_location.get()) or self.basename is not None
|
||||
|
||||
@property
|
||||
def executable(self):
|
||||
@@ -242,13 +242,13 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
try:
|
||||
if self.probe_available:
|
||||
cmd = [
|
||||
encodeFilename(self.probe_executable, True),
|
||||
self.probe_executable,
|
||||
encodeArgument('-show_streams')]
|
||||
else:
|
||||
cmd = [
|
||||
encodeFilename(self.executable, True),
|
||||
self.executable,
|
||||
encodeArgument('-i')]
|
||||
cmd.append(encodeFilename(self._ffmpeg_filename_argument(path), True))
|
||||
cmd.append(self._ffmpeg_filename_argument(path))
|
||||
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)
|
||||
@@ -281,7 +281,7 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
self.check_version()
|
||||
|
||||
cmd = [
|
||||
encodeFilename(self.probe_executable, True),
|
||||
self.probe_executable,
|
||||
encodeArgument('-hide_banner'),
|
||||
encodeArgument('-show_format'),
|
||||
encodeArgument('-show_streams'),
|
||||
@@ -334,15 +334,15 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
self.check_version()
|
||||
|
||||
oldest_mtime = min(
|
||||
os.stat(encodeFilename(path)).st_mtime for path, _ in input_path_opts if path)
|
||||
os.stat(path).st_mtime for path, _ in input_path_opts if path)
|
||||
|
||||
cmd = [encodeFilename(self.executable, True), encodeArgument('-y')]
|
||||
cmd = [self.executable, encodeArgument('-y')]
|
||||
# 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]
|
||||
keys = [f'_{name}{number}', f'_{name}']
|
||||
if name == 'o':
|
||||
args += ['-movflags', '+faststart']
|
||||
if number == 1:
|
||||
@@ -352,14 +352,14 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
args.append('-i')
|
||||
return (
|
||||
[encodeArgument(arg) for arg in args]
|
||||
+ [encodeFilename(self._ffmpeg_filename_argument(file), True)])
|
||||
+ [self._ffmpeg_filename_argument(file)])
|
||||
|
||||
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)
|
||||
|
||||
self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
|
||||
self.write_debug(f'ffmpeg command line: {shell_quote(cmd)}')
|
||||
_, stderr, returncode = Popen.run(
|
||||
cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
if returncode not in variadic(expected_retcodes):
|
||||
@@ -437,7 +437,7 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
|
||||
|
||||
class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||
COMMON_AUDIO_EXTS = MEDIA_EXTENSIONS.common_audio + ('wma', )
|
||||
COMMON_AUDIO_EXTS = (*MEDIA_EXTENSIONS.common_audio, 'wma')
|
||||
SUPPORTED_EXTS = tuple(ACODECS.keys())
|
||||
FORMAT_RE = create_mapping_re(('best', *SUPPORTED_EXTS))
|
||||
|
||||
@@ -474,7 +474,7 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||
acodec_opts = []
|
||||
else:
|
||||
acodec_opts = ['-acodec', codec]
|
||||
opts = ['-vn'] + acodec_opts + more_opts
|
||||
opts = ['-vn', *acodec_opts, *more_opts]
|
||||
try:
|
||||
FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
|
||||
except FFmpegPostProcessorError as err:
|
||||
@@ -521,9 +521,9 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||
return [], information
|
||||
orig_path = prepend_extension(path, 'orig')
|
||||
temp_path = prepend_extension(path, 'temp')
|
||||
if (self._nopostoverwrites and os.path.exists(encodeFilename(new_path))
|
||||
and os.path.exists(encodeFilename(orig_path))):
|
||||
self.to_screen('Post-process file %s exists, skipping' % new_path)
|
||||
if (self._nopostoverwrites and os.path.exists(new_path)
|
||||
and os.path.exists(orig_path)):
|
||||
self.to_screen(f'Post-process file {new_path} exists, skipping')
|
||||
return [], information
|
||||
|
||||
self.to_screen(f'Destination: {new_path}')
|
||||
@@ -626,7 +626,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
||||
sub_ext = sub_info['ext']
|
||||
if sub_ext == 'json':
|
||||
self.report_warning('JSON subtitles cannot be embedded')
|
||||
elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt':
|
||||
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'])
|
||||
@@ -641,7 +641,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
||||
if not sub_langs:
|
||||
return [], info
|
||||
|
||||
input_files = [filename] + sub_filenames
|
||||
input_files = [filename, *sub_filenames]
|
||||
|
||||
opts = [
|
||||
*self.stream_copy_opts(ext=info['ext']),
|
||||
@@ -650,15 +650,15 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
||||
'-map', '-0:s',
|
||||
]
|
||||
for i, (lang, name) in enumerate(zip(sub_langs, sub_names)):
|
||||
opts.extend(['-map', '%d:0' % (i + 1)])
|
||||
opts.extend(['-map', f'{i + 1}:0'])
|
||||
lang_code = ISO639Utils.short2long(lang) or lang
|
||||
opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
|
||||
opts.extend([f'-metadata:s:s:{i}', f'language={lang_code}'])
|
||||
if name:
|
||||
opts.extend(['-metadata:s:s:%d' % i, 'handler_name=%s' % name,
|
||||
'-metadata:s:s:%d' % i, 'title=%s' % name])
|
||||
opts.extend([f'-metadata:s:s:{i}', f'handler_name={name}',
|
||||
f'-metadata:s:s:{i}', f'title={name}'])
|
||||
|
||||
temp_filename = prepend_extension(filename, 'temp')
|
||||
self.to_screen('Embedding subtitles in "%s"' % filename)
|
||||
self.to_screen(f'Embedding subtitles in "{filename}"')
|
||||
self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
|
||||
os.replace(temp_filename, filename)
|
||||
|
||||
@@ -707,7 +707,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
return [], info
|
||||
|
||||
temp_filename = prepend_extension(filename, 'temp')
|
||||
self.to_screen('Adding metadata to "%s"' % filename)
|
||||
self.to_screen(f'Adding metadata to "{filename}"')
|
||||
self.run_ffmpeg_multiple_files(
|
||||
(filename, metadata_filename), temp_filename,
|
||||
itertools.chain(self._options(info['ext']), *options))
|
||||
@@ -728,7 +728,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
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)
|
||||
metadata_file_content += f'title={ffmpeg_escape(chapter_title)}\n'
|
||||
f.write(metadata_file_content)
|
||||
yield ('-map_metadata', '1')
|
||||
|
||||
@@ -738,11 +738,12 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
|
||||
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))
|
||||
info[key] for key in [f'{meta_prefix}_', *variadic(info_list or meta_list)]
|
||||
if info.get(key) is not None), None)
|
||||
if value not in ('', None):
|
||||
value = ', '.join(map(str, variadic(value)))
|
||||
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)})
|
||||
metadata['common'].update(dict.fromkeys(variadic(meta_list), value))
|
||||
|
||||
# Info on media metadata/metadata supported by ffmpeg:
|
||||
# https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
||||
@@ -754,10 +755,11 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
add(('description', 'synopsis'), 'description')
|
||||
add(('purl', 'comment'), 'webpage_url')
|
||||
add('track', 'track_number')
|
||||
add('artist', ('artist', 'creator', 'uploader', 'uploader_id'))
|
||||
add('genre')
|
||||
add('artist', ('artist', 'artists', 'creator', 'creators', 'uploader', 'uploader_id'))
|
||||
add('composer', ('composer', 'composers'))
|
||||
add('genre', ('genre', 'genres'))
|
||||
add('album')
|
||||
add('album_artist')
|
||||
add('album_artist', ('album_artist', 'album_artists'))
|
||||
add('disc', 'disc_number')
|
||||
add('show', 'series')
|
||||
add('season_number')
|
||||
@@ -780,7 +782,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
yield ('-metadata', f'{name}={value}')
|
||||
|
||||
stream_idx = 0
|
||||
for fmt in info.get('requested_formats') or []:
|
||||
for fmt in info.get('requested_formats') or [info]:
|
||||
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):
|
||||
@@ -805,7 +807,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
|
||||
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)
|
||||
yield ('-map', f'-0:{old_stream}')
|
||||
new_stream -= 1
|
||||
|
||||
yield (
|
||||
@@ -832,10 +834,10 @@ class FFmpegMergerPP(FFmpegPostProcessor):
|
||||
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)
|
||||
args.extend(['-map', f'{i}:v:0'])
|
||||
self.to_screen(f'Merging formats into "{filename}"')
|
||||
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args)
|
||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
||||
os.rename(temp_filename, filename)
|
||||
return info['__files_to_merge'], info
|
||||
|
||||
def can_merge(self):
|
||||
@@ -846,10 +848,9 @@ class FFmpegMergerPP(FFmpegPostProcessor):
|
||||
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, '
|
||||
warning = (f'Your copy of {self.basename} 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)
|
||||
f'Update {self.basename} to version {required_version} or newer to fix this.')
|
||||
self.report_warning(warning)
|
||||
return False
|
||||
return True
|
||||
@@ -871,7 +872,7 @@ class FFmpegFixupStretchedPP(FFmpegFixupPostProcessor):
|
||||
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])
|
||||
*self.stream_copy_opts(), '-aspect', f'{stretched_ratio:f}'])
|
||||
return [], info
|
||||
|
||||
|
||||
@@ -923,7 +924,7 @@ class FFmpegFixupTimestampPP(FFmpegFixupPostProcessor):
|
||||
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])
|
||||
self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False), '-ss', self.trim])
|
||||
return [], info
|
||||
|
||||
|
||||
@@ -968,7 +969,7 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
|
||||
continue
|
||||
ext = sub['ext']
|
||||
if ext == new_ext:
|
||||
self.to_screen('Subtitle file for %s is already in the requested format' % new_ext)
|
||||
self.to_screen(f'Subtitle file for {new_ext} is already in the requested format')
|
||||
continue
|
||||
elif ext == 'json':
|
||||
self.to_screen(
|
||||
@@ -1037,7 +1038,7 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor):
|
||||
|
||||
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)):
|
||||
if not self._downloader._ensure_dir_exists(destination):
|
||||
return
|
||||
|
||||
chapter['filepath'] = destination
|
||||
@@ -1058,7 +1059,7 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor):
|
||||
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))
|
||||
self.to_screen(f'Splitting video by chapters; {len(chapters)} chapters found')
|
||||
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())])
|
||||
@@ -1085,7 +1086,7 @@ class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor):
|
||||
_, 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)
|
||||
self.to_screen(f'Correcting thumbnail "{thumbnail_filename}" extension to webp')
|
||||
webp_filename = replace_extension(thumbnail_filename, 'webp')
|
||||
os.replace(thumbnail_filename, webp_filename)
|
||||
info['thumbnails'][idx]['filepath'] = webp_filename
|
||||
|
@@ -54,7 +54,7 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
self.write_debug('Expected and actual durations mismatch')
|
||||
|
||||
concat_opts = self._make_concat_opts(cuts, real_duration)
|
||||
self.write_debug('Concat spec = %s' % ', '.join(f'{c.get("inpoint", 0.0)}-{c.get("outpoint", "inf")}' for c in concat_opts))
|
||||
self.write_debug('Concat spec = {}'.format(', '.join(f'{c.get("inpoint", 0.0)}-{c.get("outpoint", "inf")}' for c in concat_opts)))
|
||||
|
||||
def remove_chapters(file, is_sub):
|
||||
return file, self.remove_chapters(file, cuts, concat_opts, self._force_keyframes and not is_sub)
|
||||
|
@@ -4,8 +4,6 @@ from .common import PostProcessor
|
||||
from ..compat import shutil
|
||||
from ..utils import (
|
||||
PostProcessingError,
|
||||
decodeFilename,
|
||||
encodeFilename,
|
||||
make_dir,
|
||||
)
|
||||
|
||||
@@ -21,29 +19,28 @@ class MoveFilesAfterDownloadPP(PostProcessor):
|
||||
return 'MoveFiles'
|
||||
|
||||
def run(self, info):
|
||||
dl_path, dl_name = os.path.split(encodeFilename(info['filepath']))
|
||||
dl_path, dl_name = os.path.split(info['filepath'])
|
||||
finaldir = info.get('__finaldir', dl_path)
|
||||
finalpath = os.path.join(finaldir, dl_name)
|
||||
if self._downloaded:
|
||||
info['__files_to_move'][info['filepath']] = decodeFilename(finalpath)
|
||||
info['__files_to_move'][info['filepath']] = finalpath
|
||||
|
||||
make_newfilename = lambda old: decodeFilename(os.path.join(finaldir, os.path.basename(encodeFilename(old))))
|
||||
make_newfilename = lambda old: os.path.join(finaldir, os.path.basename(old))
|
||||
for oldfile, newfile in info['__files_to_move'].items():
|
||||
if not newfile:
|
||||
newfile = make_newfilename(oldfile)
|
||||
if os.path.abspath(encodeFilename(oldfile)) == os.path.abspath(encodeFilename(newfile)):
|
||||
if os.path.abspath(oldfile) == os.path.abspath(newfile):
|
||||
continue
|
||||
if not os.path.exists(encodeFilename(oldfile)):
|
||||
self.report_warning('File "%s" cannot be found' % oldfile)
|
||||
if not os.path.exists(oldfile):
|
||||
self.report_warning(f'File "{oldfile}" cannot be found')
|
||||
continue
|
||||
if os.path.exists(encodeFilename(newfile)):
|
||||
if os.path.exists(newfile):
|
||||
if self.get_param('overwrites', True):
|
||||
self.report_warning('Replacing existing file "%s"' % newfile)
|
||||
os.remove(encodeFilename(newfile))
|
||||
self.report_warning(f'Replacing existing file "{newfile}"')
|
||||
os.remove(newfile)
|
||||
else:
|
||||
self.report_warning(
|
||||
'Cannot move file "%s" out of temporary directory since "%s" already exists. '
|
||||
% (oldfile, newfile))
|
||||
f'Cannot move file "{oldfile}" out of temporary directory since "{newfile}" already exists. ')
|
||||
continue
|
||||
make_dir(newfile, PostProcessingError)
|
||||
self.to_screen(f'Moving file "{oldfile}" to "{newfile}"')
|
||||
|
@@ -9,7 +9,6 @@ from ..utils import (
|
||||
check_executable,
|
||||
cli_option,
|
||||
encodeArgument,
|
||||
encodeFilename,
|
||||
prepend_extension,
|
||||
shell_quote,
|
||||
str_or_none,
|
||||
@@ -35,7 +34,7 @@ class SponSkrubPP(PostProcessor):
|
||||
|
||||
if not ignoreerror and self.path is None:
|
||||
if path:
|
||||
raise PostProcessingError('sponskrub not found in "%s"' % path)
|
||||
raise PostProcessingError(f'sponskrub not found in "{path}"')
|
||||
else:
|
||||
raise PostProcessingError('sponskrub not found. Please install or provide the path using --sponskrub-path')
|
||||
|
||||
@@ -52,7 +51,7 @@ class SponSkrubPP(PostProcessor):
|
||||
return [], information
|
||||
|
||||
filename = information['filepath']
|
||||
if not os.path.exists(encodeFilename(filename)): # no download
|
||||
if not os.path.exists(filename): # no download
|
||||
return [], information
|
||||
|
||||
if information['extractor_key'].lower() != 'youtube':
|
||||
@@ -71,8 +70,8 @@ class SponSkrubPP(PostProcessor):
|
||||
self.report_warning('If sponskrub is run multiple times, unintended parts of the video could be cut out.')
|
||||
|
||||
temp_filename = prepend_extension(filename, self._temp_ext)
|
||||
if os.path.exists(encodeFilename(temp_filename)):
|
||||
os.remove(encodeFilename(temp_filename))
|
||||
if os.path.exists(temp_filename):
|
||||
os.remove(temp_filename)
|
||||
|
||||
cmd = [self.path]
|
||||
if not self.cutout:
|
||||
@@ -83,7 +82,7 @@ class SponSkrubPP(PostProcessor):
|
||||
cmd += ['--', information['id'], filename, temp_filename]
|
||||
cmd = [encodeArgument(i) for i in cmd]
|
||||
|
||||
self.write_debug('sponskrub command line: %s' % shell_quote(cmd))
|
||||
self.write_debug(f'sponskrub command line: {shell_quote(cmd)}')
|
||||
stdout, _, returncode = Popen.run(cmd, text=True, stdout=None if self.get_param('verbose') else subprocess.PIPE)
|
||||
|
||||
if not returncode:
|
||||
|
@@ -27,13 +27,13 @@ class SponsorBlockPP(FFmpegPostProcessor):
|
||||
'filler': 'Filler Tangent',
|
||||
'interaction': 'Interaction Reminder',
|
||||
'music_offtopic': 'Non-Music Section',
|
||||
**NON_SKIPPABLE_CATEGORIES
|
||||
**NON_SKIPPABLE_CATEGORIES,
|
||||
}
|
||||
|
||||
def __init__(self, downloader, categories=None, api='https://sponsor.ajay.app'):
|
||||
FFmpegPostProcessor.__init__(self, downloader)
|
||||
self._categories = tuple(categories or self.CATEGORIES.keys())
|
||||
self._API_URL = api if re.match('^https?://', api) else 'https://' + api
|
||||
self._API_URL = api if re.match('https?://', api) else 'https://' + api
|
||||
|
||||
def run(self, info):
|
||||
extractor = info['extractor_key']
|
||||
@@ -57,7 +57,7 @@ class SponsorBlockPP(FFmpegPostProcessor):
|
||||
if start_end[0] <= 1:
|
||||
start_end[0] = 0
|
||||
# Make POI chapters 1 sec so that we can properly mark them
|
||||
if s['category'] in self.POI_CATEGORIES.keys():
|
||||
if s['category'] in self.POI_CATEGORIES:
|
||||
start_end[1] += 1
|
||||
# Ignore milliseconds difference at the end.
|
||||
# Never allow the segment to exceed the video.
|
||||
@@ -91,12 +91,12 @@ class SponsorBlockPP(FFmpegPostProcessor):
|
||||
return sponsor_chapters
|
||||
|
||||
def _get_sponsor_segments(self, video_id, service):
|
||||
hash = hashlib.sha256(video_id.encode('ascii')).hexdigest()
|
||||
video_hash = hashlib.sha256(video_id.encode('ascii')).hexdigest()
|
||||
# SponsorBlock API recommends using first 4 hash characters.
|
||||
url = f'{self._API_URL}/api/skipSegments/{hash[:4]}?' + urllib.parse.urlencode({
|
||||
url = f'{self._API_URL}/api/skipSegments/{video_hash[:4]}?' + urllib.parse.urlencode({
|
||||
'service': service,
|
||||
'categories': json.dumps(self._categories),
|
||||
'actionTypes': json.dumps(['skip', 'poi', 'chapter'])
|
||||
'actionTypes': json.dumps(['skip', 'poi', 'chapter']),
|
||||
})
|
||||
for d in self._download_json(url) or []:
|
||||
if d['videoID'] == video_id:
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
|
||||
from .common import PostProcessor
|
||||
from ..compat import compat_os_name
|
||||
from ..utils import (
|
||||
PostProcessingError,
|
||||
XAttrMetadataError,
|
||||
@@ -26,38 +25,40 @@ class XAttrMetadataPP(PostProcessor):
|
||||
|
||||
XATTR_MAPPING = {
|
||||
'user.xdg.referrer.url': 'webpage_url',
|
||||
# 'user.xdg.comment': 'description',
|
||||
'user.dublincore.title': 'title',
|
||||
'user.dublincore.date': 'upload_date',
|
||||
'user.dublincore.description': 'description',
|
||||
'user.dublincore.contributor': 'uploader',
|
||||
'user.dublincore.format': 'format',
|
||||
# We do this last because it may get us close to the xattr limits
|
||||
# (e.g., 4kB on ext4), and we don't want to have the other ones fail
|
||||
'user.dublincore.description': 'description',
|
||||
# 'user.xdg.comment': 'description',
|
||||
}
|
||||
|
||||
def run(self, info):
|
||||
mtime = os.stat(info['filepath']).st_mtime
|
||||
self.to_screen('Writing metadata to file\'s xattrs')
|
||||
try:
|
||||
for xattrname, infoname in self.XATTR_MAPPING.items():
|
||||
for xattrname, infoname in self.XATTR_MAPPING.items():
|
||||
try:
|
||||
value = info.get(infoname)
|
||||
if value:
|
||||
if infoname == 'upload_date':
|
||||
value = hyphenate_date(value)
|
||||
write_xattr(info['filepath'], xattrname, value.encode())
|
||||
|
||||
except XAttrUnavailableError as e:
|
||||
raise PostProcessingError(str(e))
|
||||
except XAttrMetadataError as e:
|
||||
if e.reason == 'NO_SPACE':
|
||||
self.report_warning(
|
||||
'There\'s no disk space left, disk quota exceeded or filesystem xattr limit exceeded. '
|
||||
'Some extended attributes are not written')
|
||||
elif e.reason == 'VALUE_TOO_LONG':
|
||||
self.report_warning('Unable to write extended attributes due to too long values.')
|
||||
else:
|
||||
tip = ('You need to use NTFS' if compat_os_name == 'nt'
|
||||
else 'You may have to enable them in your "/etc/fstab"')
|
||||
raise PostProcessingError(f'This filesystem doesn\'t support extended attributes. {tip}')
|
||||
except XAttrUnavailableError as e:
|
||||
raise PostProcessingError(str(e))
|
||||
except XAttrMetadataError as e:
|
||||
if e.reason == 'NO_SPACE':
|
||||
self.report_warning(
|
||||
'There\'s no disk space left, disk quota exceeded or filesystem xattr limit exceeded. '
|
||||
f'Extended attribute "{xattrname}" was not written.')
|
||||
elif e.reason == 'VALUE_TOO_LONG':
|
||||
self.report_warning(f'Unable to write extended attribute "{xattrname}" due to too long values.')
|
||||
else:
|
||||
tip = ('You need to use NTFS' if os.name == 'nt'
|
||||
else 'You may have to enable them in your "/etc/fstab"')
|
||||
raise PostProcessingError(f'This filesystem doesn\'t support extended attributes. {tip}')
|
||||
|
||||
self.try_utime(info['filepath'], mtime, mtime)
|
||||
return [], info
|
||||
|
Reference in New Issue
Block a user