From b9cb3980c2775c5c55290c44c5c07940244547e4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 8 Feb 2013 10:51:33 -0800 Subject: [PATCH] path_sep_replace config option I also took this opportunity to move and rename util.santize_for_path to library.format_for_path, which was long overdue. --- beets/config_default.yaml | 1 + beets/library.py | 44 ++++++++++++++++++++++++++++++++++----- beets/util/__init__.py | 29 -------------------------- docs/changelog.rst | 3 +++ test/test_db.py | 8 +++---- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/beets/config_default.yaml b/beets/config_default.yaml index c8696ecf9..a55d8965d 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -26,6 +26,7 @@ replace: '[<>:"\?\*\|]': _ '\.$': _ '\s+$': '' +path_sep_replace: _ art_filename: cover plugins: [] diff --git a/beets/library.py b/beets/library.py index f4bfc467f..dcd29099a 100644 --- a/beets/library.py +++ b/beets/library.py @@ -32,6 +32,7 @@ from beets import util from beets.util import bytestring_path, syspath, normpath, samefile,\ displayable_path from beets.util.functemplate import Template +import beets MAX_FILENAME_LENGTH = 200 @@ -183,6 +184,39 @@ def _regexp(expr, val): return False return res is not None +# Path element formatting for templating. +def format_for_path(value, key=None, pathmod=None): + """Sanitize the value for inclusion in a path: replace separators + with _, etc. Doesn't guarantee that the whole path will be valid; + you should still call `util.sanitize_path` on the complete path. + """ + pathmod = pathmod or os.path + + if isinstance(value, basestring): + for sep in (pathmod.sep, pathmod.altsep): + if sep: + value = value.replace( + sep, + beets.config['path_sep_replace'].get(unicode), + ) + elif key in ('track', 'tracktotal', 'disc', 'disctotal'): + # Pad indices with zeros. + value = u'%02i' % (value or 0) + elif key == 'year': + value = u'%04i' % (value or 0) + elif key in ('month', 'day'): + value = u'%02i' % (value or 0) + elif key == 'bitrate': + # Bitrate gets formatted as kbps. + value = u'%ikbps' % ((value or 0) // 1000) + elif key == 'samplerate': + # Sample rate formatted as kHz. + value = u'%ikHz' % ((value or 0) // 1000) + else: + value = unicode(value) + + return value + # Exceptions. @@ -361,7 +395,7 @@ class Item(object): # From Item. value = getattr(self, key) if sanitize: - value = util.sanitize_for_path(value, pathmod, key) + value = format_for_path(value, key, pathmod) mapping[key] = value # Additional fields in non-sanitized case. @@ -378,7 +412,7 @@ class Item(object): # Get values from plugins. for key, value in plugins.template_values(self).iteritems(): if sanitize: - value = util.sanitize_for_path(value, pathmod, key) + value = format_for_path(value, key, pathmod) mapping[key] = value # Get template functions. @@ -1568,7 +1602,7 @@ class Album(BaseAlbum): if not isinstance(self._library.art_filename,Template): self._library.art_filename = Template(self._library.art_filename) - subpath = util.sanitize_path(util.sanitize_for_path( + subpath = util.sanitize_path(format_for_path( self.evaluate_template(self._library.art_filename) )) subpath = bytestring_path(subpath) @@ -1764,8 +1798,8 @@ class DefaultTemplateFunctions(object): return res # Flatten disambiguation value into a string. - disam_value = util.sanitize_for_path(getattr(album, disambiguator), - self.pathmod, disambiguator) + disam_value = format_for_path(getattr(album, disambiguator), + disambiguator, self.pathmod) res = u' [{0}]'.format(disam_value) self.lib._memotable[memokey] = res return res diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 4d5f9fe0e..48e0c5d25 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -494,35 +494,6 @@ def truncate_path(path, pathmod=None, length=MAX_FILENAME_LENGTH): return pathmod.join(*out) -def sanitize_for_path(value, pathmod=None, key=None): - """Sanitize the value for inclusion in a path: replace separators - with _, etc. Doesn't guarantee that the whole path will be valid; - you should still call sanitize_path on the complete path. - """ - pathmod = pathmod or os.path - - if isinstance(value, basestring): - for sep in (pathmod.sep, pathmod.altsep): - if sep: - value = value.replace(sep, u'_') - elif key in ('track', 'tracktotal', 'disc', 'disctotal'): - # Pad indices with zeros. - value = u'%02i' % (value or 0) - elif key == 'year': - value = u'%04i' % (value or 0) - elif key in ('month', 'day'): - value = u'%02i' % (value or 0) - elif key == 'bitrate': - # Bitrate gets formatted as kbps. - value = u'%ikbps' % ((value or 0) // 1000) - elif key == 'samplerate': - # Sample rate formatted as kHz. - value = u'%ikHz' % ((value or 0) // 1000) - else: - value = unicode(value) - - return value - def str2bool(value): """Returns a boolean reflecting a human-entered string.""" if value.lower() in ('yes', '1', 'true', 't', 'y'): diff --git a/docs/changelog.rst b/docs/changelog.rst index 65fa95177..fbdcdbbc5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,9 @@ New configuration options: * :doc:`/plugins/lastgenre`: A new configuration option lets you choose to retrieve artist-level tags as genres instead of album- or track-level tags. Thanks to Peter Fern and Peter Schnebel. +* You can now customize the character substituted for path separators (e.g., /) + in filenames via ``path_sep_replace``. The default is an underscore. Use this + setting with caution. Other new stuff: diff --git a/test/test_db.py b/test/test_db.py index d4091cf33..d4dd30092 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -347,19 +347,19 @@ class DestinationTest(unittest.TestCase): def test_component_sanitize_replaces_separators(self): name = posixpath.join('a', 'b') - newname = util.sanitize_for_path(name, posixpath) + newname = beets.library.format_for_path(name, None, posixpath) self.assertNotEqual(name, newname) def test_component_sanitize_pads_with_zero(self): - name = util.sanitize_for_path(1, posixpath, 'track') + name = beets.library.format_for_path(1, 'track', posixpath) self.assertTrue(name.startswith('0')) def test_component_sanitize_uses_kbps_bitrate(self): - val = util.sanitize_for_path(12345, posixpath, 'bitrate') + val = beets.library.format_for_path(12345, 'bitrate', posixpath) self.assertEqual(val, u'12kbps') def test_component_sanitize_uses_khz_samplerate(self): - val = util.sanitize_for_path(12345, posixpath, 'samplerate') + val = beets.library.format_for_path(12345, 'samplerate', posixpath) self.assertEqual(val, u'12kHz') def test_artist_falls_back_to_albumartist(self):