Fix handling of output template fields with initial '-'

This commit is contained in:
df 2021-08-30 19:10:02 +01:00
parent a803582717
commit 51f781c75b
4 changed files with 66 additions and 26 deletions

View File

@ -631,6 +631,10 @@ class TestYoutubeDL(unittest.TestCase):
'height': 1080, 'height': 1080,
'title1': '$PATH', 'title1': '$PATH',
'title2': '%PATH%', 'title2': '%PATH%',
'track': '-track with initial hyphen',
'display_id': '-initial_hyphen',
'episode': '.episode with initial period',
'episode_id': '.initial_period',
} }
def fname(templ, na_placeholder='NA'): def fname(templ, na_placeholder='NA'):
@ -646,7 +650,9 @@ class TestYoutubeDL(unittest.TestCase):
self.assertEqual(fname(NA_TEST_OUTTMPL), 'NA-NA-1234.mp4') self.assertEqual(fname(NA_TEST_OUTTMPL), 'NA-NA-1234.mp4')
# Or by provided placeholder # Or by provided placeholder
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder='none'), 'none-none-1234.mp4') self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder='none'), 'none-none-1234.mp4')
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '--1234.mp4') self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '_-1234.mp4')
NA_TEST_OUTTMPL = '%(uploader_date)s+%(width)d+%(id)s.%(ext)s'
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '++1234.mp4')
self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4') self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4')
self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4') self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4')
self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4') self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4')
@ -664,6 +670,16 @@ class TestYoutubeDL(unittest.TestCase):
self.assertEqual(fname('%%(width)06d.%(ext)s'), '%(width)06d.mp4') self.assertEqual(fname('%%(width)06d.%(ext)s'), '%(width)06d.mp4')
self.assertEqual(fname('Hello %(title1)s'), 'Hello $PATH') self.assertEqual(fname('Hello %(title1)s'), 'Hello $PATH')
self.assertEqual(fname('Hello %(title2)s'), 'Hello %PATH%') self.assertEqual(fname('Hello %(title2)s'), 'Hello %PATH%')
self.assertEqual(fname('%(track)s at start changes hyphen'),
'_track with initial hyphen at start changes hyphen')
self.assertEqual(fname('medial %(track)s doesn\'t change hyphen'),
'medial -track with initial hyphen doesn\'t change hyphen')
self.assertEqual(fname('%(display_id)s at start doesn\'t change hyphen'),
'-initial_hyphen at start doesn\'t change hyphen')
self.assertEqual(fname('%(episode)s at start changes period'),
'_episode with initial period at start changes period')
self.assertEqual(fname('%(episode_id)s at start doesn\'t change period'),
'.initial_period at start doesn\'t change period')
def test_format_note(self): def test_format_note(self):
ydl = YoutubeDL() ydl = YoutubeDL()

View File

@ -146,10 +146,8 @@ class TestUtil(unittest.TestCase):
sanitize_filename('New World record at 0:12:34'), sanitize_filename('New World record at 0:12:34'),
'New World record at 0_12_34') 'New World record at 0_12_34')
self.assertEqual(sanitize_filename('--gasdgf'), '_-gasdgf') self.assertEqual(sanitize_filename('__gas__dgf_'), 'gas_dgf')
self.assertEqual(sanitize_filename('--gasdgf', is_id=True), '--gasdgf') self.assertEqual(sanitize_filename('__gas__dgf_', is_id=True), '__gas__dgf_')
self.assertEqual(sanitize_filename('.gasdgf'), 'gasdgf')
self.assertEqual(sanitize_filename('.gasdgf', is_id=True), '.gasdgf')
forbidden = '"\0\\/' forbidden = '"\0\\/'
for fc in forbidden: for fc in forbidden:

View File

@ -634,6 +634,20 @@ class YoutubeDL(object):
except UnicodeEncodeError: except UnicodeEncodeError:
self.to_screen('[download] The file has already been downloaded') self.to_screen('[download] The file has already been downloaded')
# As of [1] format syntax is:
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
_FORMAT_RE = r'''(?x)
(?<!%)
%
\({0}\) # mapping key
(?:[#0\-+ ]+)? # conversion flags (optional)
(?:\d+)? # minimum field width (optional)
(?:\.\d+)? # precision (optional)
[hlL]? # length modifier (optional)
[diouxXeEfFgGcrs%] # conversion type
'''
def prepare_filename(self, info_dict): def prepare_filename(self, info_dict):
"""Generate the output filename.""" """Generate the output filename."""
try: try:
@ -652,10 +666,11 @@ class YoutubeDL(object):
elif template_dict.get('width'): elif template_dict.get('width'):
template_dict['resolution'] = '%dx?' % template_dict['width'] template_dict['resolution'] = '%dx?' % template_dict['width']
is_id = lambda k: k == 'id' or k.endswith('_id')
sanitize = lambda k, v: sanitize_filename( sanitize = lambda k, v: sanitize_filename(
compat_str(v), compat_str(v),
restricted=self.params.get('restrictfilenames'), restricted=self.params.get('restrictfilenames'),
is_id=(k == 'id' or k.endswith('_id'))) is_id=is_id(k))
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v)) template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
for k, v in template_dict.items() for k, v in template_dict.items()
if v is not None and not isinstance(v, (list, tuple, dict))) if v is not None and not isinstance(v, (list, tuple, dict)))
@ -683,21 +698,8 @@ class YoutubeDL(object):
# output template for missing fields to meet string presentation type. # output template for missing fields to meet string presentation type.
for numeric_field in self._NUMERIC_FIELDS: for numeric_field in self._NUMERIC_FIELDS:
if numeric_field not in template_dict: if numeric_field not in template_dict:
# As of [1] format syntax is:
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
FORMAT_RE = r'''(?x)
(?<!%)
%
\({0}\) # mapping key
(?:[#0\-+ ]+)? # conversion flags (optional)
(?:\d+)? # minimum field width (optional)
(?:\.\d+)? # precision (optional)
[hlL]? # length modifier (optional)
[diouxXeEfFgGcrs%] # conversion type
'''
outtmpl = re.sub( outtmpl = re.sub(
FORMAT_RE.format(numeric_field), self._FORMAT_RE.format(numeric_field),
r'%({0})s'.format(numeric_field), outtmpl) r'%({0})s'.format(numeric_field), outtmpl)
# expand_path translates '%%' into '%' and '$$' into '$' # expand_path translates '%%' into '%' and '$$' into '$'
@ -711,7 +713,35 @@ class YoutubeDL(object):
# because meta fields may contain env variables we don't want to # because meta fields may contain env variables we don't want to
# be expanded. For example, for outtmpl "%(title)s.%(ext)s" and # be expanded. For example, for outtmpl "%(title)s.%(ext)s" and
# title "Hello $PATH", we don't want `$PATH` to be expanded. # title "Hello $PATH", we don't want `$PATH` to be expanded.
filename = expand_path(outtmpl).replace(sep, '') % template_dict filename = expand_path(outtmpl).replace(sep, '')
# avoid non-mandatory (#571) initial '-' (#5035): convert to '_'
# also remove initial '.'
# first split the filename around the format fields
FORMAT_SPLIT_RE = (
'%s(%s)' % tuple(
re.split(r'(\(\?[iLmsux]+\))',
self._FORMAT_RE.format(r'\w+'), maxsplit=1)[1:]))
filename = re.split(FORMAT_SPLIT_RE, filename)
# tag any filename fragment that is an ID format
FORMAT_FIELD_RE = (
'%s(%s)' % tuple(
re.split(r'(\(\?[iLmsux]+\))',
self._FORMAT_RE.format(r'(?P<field>\w+)'), maxsplit=1)[1:]))
field_is_id = (
lambda x:
(lambda y: y and is_id(y.group('field')))(re.match(FORMAT_FIELD_RE, x)))
# list formatted fragments and tags
tagged_fn = [(x % template_dict, field_is_id(x)) for x in filename]
# list the initial fragments that can be munged from [.-] to _, ie
# those that not IDs, or empty
mungable = itertools.takewhile(lambda x: not(x[1] and x[0]), tagged_fn)
col0 = lambda l: next(iter(zip(*l)), [])
mungable = col0(mungable)
# finally combine the munged initial part and the rest
filename = (re.sub(r'^[.-]', '_', ''.join(mungable))
+ ''.join(col0(tagged_fn[len(mungable):])))
# Temporary fix for #4787 # Temporary fix for #4787
# 'Treat' all problem characters by passing filename through preferredencoding # 'Treat' all problem characters by passing filename through preferredencoding

View File

@ -2104,15 +2104,11 @@ def sanitize_filename(s, restricted=False, is_id=False):
s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s) s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
result = ''.join(map(replace_insane, s)) result = ''.join(map(replace_insane, s))
if not is_id: if not is_id:
while '__' in result: result = re.sub(r'_{2,}', '_', result)
result = result.replace('__', '_')
result = result.strip('_') result = result.strip('_')
# Common case of "Foreign band name - English song title" # Common case of "Foreign band name - English song title"
if restricted and result.startswith('-_'): if restricted and result.startswith('-_'):
result = result[2:] result = result[2:]
if result.startswith('-'):
result = '_' + result[len('-'):]
result = result.lstrip('.')
if not result: if not result:
result = '_' result = '_'
return result return result