diff --git a/beets/library.py b/beets/library.py index 12e16e07c..f8a70fe2a 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1152,7 +1152,6 @@ class Library(BaseLibrary): # Encode for the filesystem. if not fragment: subpath = bytestring_path(subpath) - subpath = util.truncate_path(subpath, pathmod) # Preserve extension. _, extension = pathmod.splitext(item.path) @@ -1161,6 +1160,9 @@ class Library(BaseLibrary): extension = extension.decode('utf8', 'ignore') subpath += extension.lower() + # Truncate too-long components. + subpath = util.truncate_path(subpath, pathmod) + if fragment: return subpath else: diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 2b76a5dd0..d798cb102 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -457,13 +457,22 @@ def sanitize_path(path, pathmod=None, replacements=None): comps[i] = comp return pathmod.join(*comps) -def truncate_path(path, pathmod=None): +def truncate_path(path, pathmod=None, length=MAX_FILENAME_LENGTH): """Given a bytestring path or a Unicode path fragment, truncate the - components to a legal length. + components to a legal length. In the last component, the extension + is preserved. """ pathmod = pathmod or os.path - comps = [c[:MAX_FILENAME_LENGTH] for c in components(path, pathmod)] - return pathmod.join(*comps) + comps = components(path, pathmod) + + out = [c[:length] for c in comps] + base, ext = pathmod.splitext(comps[-1]) + if ext: + # Last component has an extension. + base = base[:length - len(ext)] + out[-1] = base + ext + + return pathmod.join(*out) def sanitize_for_path(value, pathmod, key=None): """Sanitize the value for inclusion in a path: replace separators diff --git a/docs/changelog.rst b/docs/changelog.rst index 76c3daf37..d65326b65 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -60,6 +60,7 @@ Changelog question is now logged (thanks to Mike Kazantsev). * Truncate long filenames based on their *bytes* rather than their Unicode *characters*, fixing situations where encoded names could be too long. +* Filename truncation now incorporates the length of the extension. * Fix an assertion failure when the MusicBrainz main database and search server disagree. * Fix a bug that caused the :doc:`/plugins/lastgenre` and other plugins not to diff --git a/test/test_db.py b/test/test_db.py index 5df34f45e..dc1d0ce0f 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -918,6 +918,19 @@ class PathStringTest(unittest.TestCase): alb = self.lib.get_album(alb.id) self.assert_(isinstance(alb.artpath, str)) +class PathTruncationTest(unittest.TestCase): + def test_truncate_bytestring(self): + p = util.truncate_path('abcde/fgh', posixpath, 4) + self.assertEqual(p, 'abcd/fgh') + + def test_truncate_unicode(self): + p = util.truncate_path(u'abcde/fgh', posixpath, 4) + self.assertEqual(p, u'abcd/fgh') + + def test_truncate_preserves_extension(self): + p = util.truncate_path(u'abcde/fgh.ext', posixpath, 5) + self.assertEqual(p, u'abcde/f.ext') + class MtimeTest(unittest.TestCase): def setUp(self): self.ipath = os.path.join(_common.RSRC, 'testfile.mp3')