From 4f46e5a323d172be690e809e48db63c35152d665 Mon Sep 17 00:00:00 2001 From: Teika Kazura Date: Sun, 23 Feb 2025 13:59:55 +0900 Subject: [PATCH 1/2] YoutubeDL.py: Defines a new function validate_destination_filename(), which checks the filename length limit before downloading. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This closes issue #8908 """yt-dlp should abort on first "File name too long" error""". Currenty the test fails at test/test_YoutubeDL.py:819. The message is: ------------------------------------------------------------------------- TestYoutubeDL::test_prepare_outtmpl_and_filename - OSError: [Errno 36] File name too long: '{"id ": "1234", "ext": "mp4", "width": null,... ------------------------------------------------------------------------- And the tried filename is: ------------------------------------------------------------------------ '{"id": "1234", "ext": "mp4", "width": null, "height": 1080, "filesize": 1024, "title1": "$PATH", "title2": "%PATH%", "title3": "foo⧸bar⧹⧹test", "title4": "foo ⧹"ba⧹" test", "title5": "⧹u00e1⧹u00e9⧹u00ed ⧹ud835⧹udc00", "timestamp": 1618488000, "duration: 100000, "playlist_index": 1, "playlist_autonumber": 2, "__last_playlist_index": 100, "n_entries": 10, "formats": [{"id": "id 1", "height": 1080, "width": 1920}, {"id": "id2", "height": 720}, {"id": "id 3"}], "epoch": 1740284668, "duration_string": "27-46-40", "autonumber": 1, "video_autonumber": 0, "resolution": "1080p"}' ------------------------------------------------------------------------ --- yt_dlp/YoutubeDL.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 8790b326b7..1024911c31 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -1475,12 +1475,41 @@ class YoutubeDL: self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')') return None + def validate_destination_filename(self, filename): + """Check if the destination filename is valid in the OS. In particular +it checks if the length does not exceed the OS limit. However currently +any error is simply raised, independent of the cause. + +Without this, download would fail only after the entire file is downloaded.""" + cwd = os.getcwd() + with tempfile.TemporaryDirectory() as d: + os.chdir(d) + try: + with open(filename, 'w') as f: + f.close() + except OSError as e: + if (os.name == 'nt' and e.errno == 206) or (e.errno == errno.ENAMETOOLONG): + # The first condition is for windows, + # and the second for unix-ish systems. + + # An improvement idea: + # by default, retry (exec yt-dlp itself) by + # -o "%(id)s.%(ext)s" --write-info-json, + # but respect the directory from --output of the original call. + self.to_screen('''[Notice] The file name to be saved is too long, exceeding the OS limit. +[Notice] Consider options --trim-filenames or -o (--output).''') + + raise + finally: + os.chdir(cwd) + def prepare_filename(self, info_dict, dir_type='', *, outtmpl=None, warn=False): """Generate the output filename""" if outtmpl: assert not dir_type, 'outtmpl and dir_type are mutually exclusive' dir_type = None filename = self._prepare_filename(info_dict, tmpl_type=dir_type, outtmpl=outtmpl) + self.validate_destination_filename(filename) if not filename and dir_type not in ('', 'temp'): return '' From 52d0130a37f144b1b5b7bccb8c99945d068bee78 Mon Sep 17 00:00:00 2001 From: Teika Kazura Date: Tue, 11 Mar 2025 18:21:47 +0900 Subject: [PATCH 2/2] * YoutubeDL:validate_destination_filename(): Bugfix of the previous commit when -o contains a directory. * test_YoutubeDL:test_prepare_outtmpl_and_filename(): Prevents bogus test failure. --- test/test_YoutubeDL.py | 1 + yt_dlp/YoutubeDL.py | 43 +++++++++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/test/test_YoutubeDL.py b/test/test_YoutubeDL.py index 708a04f92d..c742e4eb4e 100644 --- a/test/test_YoutubeDL.py +++ b/test/test_YoutubeDL.py @@ -742,6 +742,7 @@ class TestYoutubeDL(unittest.TestCase): params['outtmpl'] = tmpl ydl = FakeYDL(params) ydl._num_downloads = 1 + ydl.validate_destination_filename = lambda *args: None self.assertEqual(ydl.validate_outtmpl(tmpl), None) out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 1024911c31..44f943800d 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -12,6 +12,7 @@ import json import locale import operator import os +import pathlib import random import re import shutil @@ -1481,27 +1482,39 @@ it checks if the length does not exceed the OS limit. However currently any error is simply raised, independent of the cause. Without this, download would fail only after the entire file is downloaded.""" - cwd = os.getcwd() + # An improvement idea: + # by default, retry (exec yt-dlp itself) by + # -o "%(id)s.%(ext)s" --write-info-json, + # but respect the directory from --output of the original call. + with tempfile.TemporaryDirectory() as d: - os.chdir(d) try: - with open(filename, 'w') as f: - f.close() + # To make sure it's confined under tmpdir + tmpfn = '.' + os.sep + os.path.splitdrive(filename)[1] + + parentDirStr = os.sep + '..' + os.sep + # This may contain '../' so remove them. + while parentDirStr in tmpfn: + tmpfn = tmpfn.replace(parentDirStr, os.sep) + tmpfn = os.path.join(d, tmpfn) + pathlib.Path(os.path.dirname(tmpfn)).mkdir(parents=True, exist_ok=True) + open(tmpfn, 'w').close() except OSError as e: - if (os.name == 'nt' and e.errno == 206) or (e.errno == errno.ENAMETOOLONG): - # The first condition is for windows, - # and the second for unix-ish systems. - - # An improvement idea: - # by default, retry (exec yt-dlp itself) by - # -o "%(id)s.%(ext)s" --write-info-json, - # but respect the directory from --output of the original call. + if (os.name == 'nt' and e.errno == 206) or (os.name != 'nt' and e.errno == errno.ENAMETOOLONG): + # For Win, 206 means filename length exceeds MAX_PATH. + + e.filename = filename self.to_screen('''[Notice] The file name to be saved is too long, exceeding the OS limit. [Notice] Consider options --trim-filenames or -o (--output).''') - raise - finally: - os.chdir(cwd) + elif os.name == 'nt' and e.errno == 22: + # Even when MAX_PATH is disabled, 255 chars is the limit, resulting in 22. + # https://github.com/python/cpython/issues/126929#issuecomment-2483684861 + e.filename = filename + self.to_screen(f'''[Notice] Attempt to create file {filename} resulted in Errno 22. +This is often caused e.g. by too long filename or forbidden characters.''') + + raise def prepare_filename(self, info_dict, dir_type='', *, outtmpl=None, warn=False): """Generate the output filename"""