diff options
Diffstat (limited to 'yt_dlp/YoutubeDL.py')
-rw-r--r-- | yt_dlp/YoutubeDL.py | 67 |
1 files changed, 40 insertions, 27 deletions
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index c0bde4339..3350042c9 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -65,7 +65,8 @@ from .utils import ( float_or_none, format_bytes, format_field, - STR_FORMAT_RE, + STR_FORMAT_RE_TMPL, + STR_FORMAT_TYPES, formatSeconds, GeoRestrictedError, HEADRequest, @@ -845,20 +846,40 @@ class YoutubeDL(object): 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') @@ -879,7 +900,7 @@ class YoutubeDL(object): } 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__, @@ -938,10 +959,11 @@ class YoutubeDL(object): 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': ''} @@ -965,6 +987,7 @@ class YoutubeDL(object): 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 @@ -972,9 +995,10 @@ class YoutubeDL(object): 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 @@ -986,19 +1010,8 @@ class YoutubeDL(object): 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: @@ -2344,7 +2357,7 @@ class YoutubeDL(object): 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') |