diff --git a/test/test_YoutubeDL.py b/test/test_YoutubeDL.py index 708a04f92d..1c48d88625 100644 --- a/test/test_YoutubeDL.py +++ b/test/test_YoutubeDL.py @@ -2,6 +2,7 @@ # Allow direct execution import os +import platform import sys import unittest from unittest.mock import patch @@ -724,6 +725,7 @@ class TestYoutubeDL(unittest.TestCase): 'title3': 'foo/bar\\test', 'title4': 'foo "bar" test', 'title5': 'áéí 𝐀', + 'title6': 'あ' * 10, 'timestamp': 1618488000, 'duration': 100000, 'playlist_index': 1, @@ -739,12 +741,14 @@ class TestYoutubeDL(unittest.TestCase): def test_prepare_outtmpl_and_filename(self): def test(tmpl, expected, *, info=None, **params): + if 'trim_file_name' not in params: + params['trim_file_name'] = 'none' # disable trimming params['outtmpl'] = tmpl ydl = FakeYDL(params) ydl._num_downloads = 1 self.assertEqual(ydl.validate_outtmpl(tmpl), None) - out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info) + out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info, trim_filename=True) fname = ydl.prepare_filename(info or self.outtmpl_info) if not isinstance(expected, (list, tuple)): @@ -951,6 +955,20 @@ class TestYoutubeDL(unittest.TestCase): test('%(title3)s', ('foo/bar\\test', 'foo⧸bar⧹test')) test('folder/%(title3)s', ('folder/foo/bar\\test', f'folder{os.path.sep}foo⧸bar⧹test')) + # --trim-filenames + test('%(title6)s.%(ext)s', 'あ' * 10 + '.mp4') + test('%(title6)s.%(ext)s', 'あ' * 3 + '.mp4', trim_file_name='3c') + if sys.getfilesystemencoding() == 'utf-8' and platform.system() != 'Windows': + test('%(title6)s.%(ext)s', 'あ' * 3 + '.mp4', trim_file_name='9b') + test('%(title6)s.%(ext)s', 'あ' * 3 + '.mp4', trim_file_name='10b') + test('%(title6)s.%(ext)s', 'あ' * 3 + '.mp4', trim_file_name='11b') + test('%(title6)s.%(ext)s', 'あ' * 4 + '.mp4', trim_file_name='12b') + elif platform.system() == 'Windows': + test('%(title6)s.%(ext)s', 'あ' * 4 + '.mp4', trim_file_name='8b') + test('%(title6)s.%(ext)s', 'あ' * 4 + '.mp4', trim_file_name='9b') + test('%(title6)s.%(ext)s', 'あ' * 5 + '.mp4', trim_file_name='10b') + test('folder/%(title6)s.%(ext)s', f'fol{os.path.sep}あああ.mp4', trim_file_name='3c') + def test_format_note(self): ydl = YoutubeDL() self.assertEqual(ydl._format_note({}), '') diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 63e6e11b26..17f836c6a0 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -12,6 +12,8 @@ import json import locale import operator import os +from pathlib import Path +import platform import random import re import shutil @@ -1442,9 +1444,42 @@ class YoutubeDL: return EXTERNAL_FORMAT_RE.sub(create_key, outtmpl), TMPL_DICT - def evaluate_outtmpl(self, outtmpl, info_dict, *args, **kwargs): + def evaluate_outtmpl(self, outtmpl, info_dict, *args, trim_filename=False, **kwargs): outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs) - return self.escape_outtmpl(outtmpl) % info_dict + if not trim_filename: + return self.escape_outtmpl(outtmpl) % info_dict + + ext_suffix = '.%(ext\0s)s' + suffix = '' + if outtmpl.endswith(ext_suffix): + outtmpl = outtmpl[:-len(ext_suffix)] + suffix = ext_suffix % info_dict + outtmpl = self.escape_outtmpl(outtmpl) + filename = outtmpl % info_dict + + def parse_trim_file_name(trim_file_name): + if trim_file_name is None or trim_file_name == 'none': + return 0, None + mobj = re.match(r'(?:(?P\d+)(?Pb|c)?|none)', trim_file_name) + return int(mobj.group('length')), mobj.group('mode') or 'c' + + max_file_name, mode = parse_trim_file_name(self.params.get('trim_file_name')) + if max_file_name == 0: + # no maximum + return filename + suffix + + encoding = sys.getfilesystemencoding() if platform.system() != 'Windows' else 'utf-16-le' + + def trim_filename(name: str): + if mode == 'b': + name = name.encode(encoding) + name = name[:max_file_name] + return name.decode(encoding, 'ignore') + else: + return name[:max_file_name] + + filename = os.path.join(*map(trim_filename, Path(filename).parts or '.')) + return filename + suffix @_catch_unsafe_extension_error def _prepare_filename(self, info_dict, *, outtmpl=None, tmpl_type=None): @@ -1453,7 +1488,7 @@ class YoutubeDL: outtmpl = self.params['outtmpl'].get(tmpl_type or 'default', self.params['outtmpl']['default']) try: outtmpl = self._outtmpl_expandpath(outtmpl) - filename = self.evaluate_outtmpl(outtmpl, info_dict, True) + filename = self.evaluate_outtmpl(outtmpl, info_dict, True, trim_filename=True) if not filename: return None @@ -1465,13 +1500,6 @@ class YoutubeDL: force_ext = OUTTMPL_TYPES[tmpl_type] if force_ext: filename = replace_extension(filename, force_ext, info_dict.get('ext')) - - # https://github.com/blackjack4494/youtube-dlc/issues/85 - trim_file_name = self.params.get('trim_file_name', False) - if trim_file_name: - no_ext, *ext = filename.rsplit('.', 2) - filename = join_nonempty(no_ext[:trim_file_name], *ext, delim='.') - return filename except ValueError as err: self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')') diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 714d9ad5c2..9bf83b2d65 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -436,6 +436,8 @@ def validate_options(opts): if opts.plugin_dirs is None: opts.plugin_dirs = ['default'] + validate_regex('trim filenames', opts.trim_file_name, r'(?:\d+[bc]?|none)') + if opts.playlist_items is not None: try: tuple(PlaylistEntries.parse_playlist_items(opts.playlist_items)) diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 1742cbdfaf..222c5e361c 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -1388,8 +1388,8 @@ def create_parser(): help='Sanitize filenames only minimally') filesystem.add_option( '--trim-filenames', '--trim-file-names', metavar='LENGTH', - dest='trim_file_name', default=0, type=int, - help='Limit the filename length (excluding extension) to the specified number of characters') + dest='trim_file_name', default='none', + help='Limit the filename length (excluding extension) to the specified number of characters or bytes') filesystem.add_option( '-w', '--no-overwrites', action='store_false', dest='overwrites', default=None,