diff --git a/test/test_YoutubeDL.py b/test/test_YoutubeDL.py index 8d796bcdd..c02bfadfc 100644 --- a/test/test_YoutubeDL.py +++ b/test/test_YoutubeDL.py @@ -664,15 +664,15 @@ def test_add_extra_info(self): } def test_prepare_outtmpl_and_filename(self): - def test(tmpl, expected, **params): + def test(tmpl, expected, *, info=None, **params): params['outtmpl'] = tmpl ydl = YoutubeDL(params) ydl._num_downloads = 1 self.assertEqual(ydl.validate_outtmpl(tmpl), None) - outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, self.outtmpl_info) + outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, info or self.outtmpl_info) out = outtmpl % tmpl_dict - fname = ydl.prepare_filename(self.outtmpl_info) + fname = ydl.prepare_filename(info or self.outtmpl_info) if callable(expected): self.assertTrue(expected(out)) @@ -700,6 +700,15 @@ def test(tmpl, expected, **params): test('%(width)06d.%%(ext)s', 'NA.%(ext)s') test('%%(width)06d.%(ext)s', '%(width)06d.mp4') + # ID sanitization + test('%(id)s', '_abcd', info={'id': '_abcd'}) + test('%(some_id)s', '_abcd', info={'some_id': '_abcd'}) + test('%(formats.0.id)s', '_abcd', info={'formats': [{'id': '_abcd'}]}) + test('%(id)s', '-abcd', info={'id': '-abcd'}) + test('%(id)s', '.abcd', info={'id': '.abcd'}) + test('%(id)s', 'ab__cd', info={'id': 'ab__cd'}) + 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)) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index f60b7eec9..bf3eef67b 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -934,7 +934,7 @@ def create_key(outer_mobj): fmt = outer_mobj.group('format') mobj = re.match(INTERNAL_FORMAT_RE, key) if mobj is None: - value, default = None, na + value, default, mobj = None, na, {'fields': ''} else: mobj = mobj.groupdict() default = mobj['default'] if mobj['default'] is not None else na @@ -944,7 +944,6 @@ def create_key(outer_mobj): fmt = '0{:d}d'.format(field_size_compat_map[key]) value = default if value is None else value - key += '\0%s' % fmt if fmt == 'c': value = compat_str(value) @@ -962,7 +961,8 @@ def create_key(outer_mobj): # So we convert it to repr first value, fmt = repr(value), '%ss' % fmt[:-1] if fmt[-1] in 'csr': - value = sanitize(key, value) + value = sanitize(mobj['fields'].split('.')[-1], value) + key += '\0%s' % fmt TMPL_DICT[key] = value return '%({key}){fmt}'.format(key=key, fmt=fmt) diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 59445a1da..8e85620cc 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -6241,6 +6241,8 @@ def traverse_obj(obj, keys, *, casesense=True, is_user_input=False, traverse_str if is_user_input: key = (int_or_none(key) if ':' not in key else slice(*map(int_or_none, key.split(':')))) + if key is None: + return None if not isinstance(obj, (list, tuple)): if traverse_string: obj = compat_str(obj)