from test.helper import FakeYDL, assertRegexpMatches
from yt_dlp import YoutubeDL
-from yt_dlp.compat import compat_str, compat_urllib_error
+from yt_dlp.compat import compat_os_name, compat_setenv, compat_str, compat_urllib_error
from yt_dlp.extractor import YoutubeIE
from yt_dlp.extractor.common import InfoExtractor
from yt_dlp.postprocessor.common import PostProcessor
self.assertEqual(ydl.validate_outtmpl(tmpl), None)
outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, info or self.outtmpl_info)
- out = outtmpl % tmpl_dict
+ out = ydl.escape_outtmpl(outtmpl) % tmpl_dict
fname = ydl.prepare_filename(info or self.outtmpl_info)
if callable(expected):
test('%(autonumber)s', '001', autonumber_size=3)
# Escaping %
+ test('%', '%')
test('%%', '%')
test('%%%%', '%%')
+ test('%s', '%s')
+ test('%%%s', '%%s')
+ test('%d', '%d')
+ test('%abc%', '%abc%')
test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
+ test('%%%(height)s', '%1080')
test('%(width)06d.%(ext)s', 'NA.mp4')
test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
test('%(id)s', ('ab:cd', 'ab -cd'), info={'id': 'ab:cd'})
# Invalid templates
- self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%'), ValueError))
self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%(title)'), ValueError))
test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder='none')
test('%()s', 'NA')
- test('%s', '%s')
- test('%d', '%d')
# NA placeholder
NA_TEST_OUTTMPL = '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
# Internal formatting
FORMATS = self.outtmpl_info['formats']
test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
+ test('%(title|%)s %(title|%%)s', '% %%')
test('%(id+1-height+3)05d', '00158')
test('%(width+100)05d', 'NA')
test('%(formats.0) 15s', ('% 15s' % FORMATS[0], '% 15s' % str(FORMATS[0]).replace(':', ' -')))
# test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # fixme
# test('%(foo|)s', ('', '_')) # fixme
+ # Environment variable expansion for prepare_filename
+ compat_setenv('__yt_dlp_var', 'expanded')
+ envvar = '%__yt_dlp_var%' if compat_os_name == 'nt' else '$__yt_dlp_var'
+ test(envvar, (envvar, 'expanded'))
+
# Path expansion and escaping
test('Hello %(title1)s', 'Hello $PATH')
test('Hello %(title2)s', 'Hello %PATH%')
float_or_none,
format_bytes,
format_field,
- STR_FORMAT_RE,
+ STR_FORMAT_RE_TMPL,
+ STR_FORMAT_TYPES,
formatSeconds,
GeoRestrictedError,
HEADRequest,
return sanitize_path(path, force=self.params.get('windowsfilenames'))
@staticmethod
- def validate_outtmpl(tmpl):
+ def _outtmpl_expandpath(outtmpl):
+ # expand_path translates '%%' into '%' and '$$' into '$'
+ # correspondingly that is not what we want since we need to keep
+ # '%%' intact for template dict substitution step. Working around
+ # with boundary-alike separator hack.
+ sep = ''.join([random.choice(ascii_letters) for _ in range(32)])
+ outtmpl = outtmpl.replace('%%', '%{0}%'.format(sep)).replace('$$', '${0}$'.format(sep))
+
+ # outtmpl should be expand_path'ed before template dict substitution
+ # because meta fields may contain env variables we don't want to
+ # be expanded. For example, for outtmpl "%(title)s.%(ext)s" and
+ # title "Hello $PATH", we don't want `$PATH` to be expanded.
+ return expand_path(outtmpl).replace(sep, '')
+
+ @staticmethod
+ def escape_outtmpl(outtmpl):
+ ''' Escape any remaining strings like %s, %abc% etc. '''
+ return re.sub(
+ STR_FORMAT_RE_TMPL.format('', '(?![%(\0])'),
+ lambda mobj: ('' if mobj.group('has_key') else '%') + mobj.group(0),
+ outtmpl)
+
+ @classmethod
+ def validate_outtmpl(cls, outtmpl):
''' @return None or Exception object '''
+ outtmpl = cls.escape_outtmpl(cls._outtmpl_expandpath(outtmpl))
try:
- re.sub(
- STR_FORMAT_RE.format(''),
- lambda mobj: ('%' if not mobj.group('has_key') else '') + mobj.group(0),
- tmpl
- ) % collections.defaultdict(int)
+ outtmpl % collections.defaultdict(int)
return None
except ValueError as err:
return err
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=None):
- """ Make the template and info_dict suitable for substitution (outtmpl % info_dict)"""
+ """ Make the template and info_dict suitable for substitution : ydl.outtmpl_escape(outtmpl) % info_dict """
info_dict = dict(info_dict)
na = self.params.get('outtmpl_na_placeholder', 'NA')
}
TMPL_DICT = {}
- EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE.format('[^)]*'))
+ EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE_TMPL.format('[^)]*', f'[{STR_FORMAT_TYPES}]'))
MATH_FUNCTIONS = {
'+': float.__add__,
'-': float.__sub__,
def create_key(outer_mobj):
if not outer_mobj.group('has_key'):
- return '%{}'.format(outer_mobj.group(0))
+ return f'%{outer_mobj.group(0)}'
+ prefix = outer_mobj.group('prefix')
key = outer_mobj.group('key')
- fmt = outer_mobj.group('format')
+ original_fmt = fmt = outer_mobj.group('format')
mobj = re.match(INTERNAL_FORMAT_RE, key)
if mobj is None:
value, default, mobj = None, na, {'fields': ''}
value = float_or_none(value)
if value is None:
value, fmt = default, 's'
+
if sanitize:
if fmt[-1] == 'r':
# If value is an object, sanitize might convert it to a string
value, fmt = repr(value), '%ss' % fmt[:-1]
if fmt[-1] in 'csr':
value = sanitize(mobj['fields'].split('.')[-1], value)
- key += '\0%s' % fmt
+
+ key = '%s\0%s' % (key.replace('%', '%\0'), original_fmt)
TMPL_DICT[key] = value
- return '%({key}){fmt}'.format(key=key, fmt=fmt)
+ return f'{prefix}%({key}){fmt}'
return EXTERNAL_FORMAT_RE.sub(create_key, outtmpl), TMPL_DICT
is_id=(k == 'id' or k.endswith('_id')))
outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default'])
outtmpl, template_dict = self.prepare_outtmpl(outtmpl, info_dict, sanitize)
-
- # expand_path translates '%%' into '%' and '$$' into '$'
- # correspondingly that is not what we want since we need to keep
- # '%%' intact for template dict substitution step. Working around
- # with boundary-alike separator hack.
- sep = ''.join([random.choice(ascii_letters) for _ in range(32)])
- outtmpl = outtmpl.replace('%%', '%{0}%'.format(sep)).replace('$$', '${0}$'.format(sep))
-
- # outtmpl should be expand_path'ed before template dict substitution
- # because meta fields may contain env variables we don't want to
- # be expanded. For example, for outtmpl "%(title)s.%(ext)s" and
- # title "Hello $PATH", we don't want `$PATH` to be expanded.
- filename = expand_path(outtmpl).replace(sep, '') % template_dict
+ outtmpl = self.escape_outtmpl(self._outtmpl_expandpath(outtmpl))
+ filename = outtmpl % template_dict
force_ext = OUTTMPL_TYPES.get(tmpl_type)
if force_ext is not None:
if re.match(r'\w+$', tmpl):
tmpl = '%({})s'.format(tmpl)
tmpl, info_copy = self.prepare_outtmpl(tmpl, info_dict)
- self.to_stdout(tmpl % info_copy)
+ self.to_stdout(self.escape_outtmpl(tmpl) % info_copy)
print_mandatory('title')
print_mandatory('id')