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.
This commit is contained in:
Adrian Sampson 2013-02-08 10:51:33 -08:00
parent 09d6eedd6a
commit b9cb3980c2
5 changed files with 47 additions and 38 deletions

View file

@ -26,6 +26,7 @@ replace:
'[<>:"\?\*\|]': _
'\.$': _
'\s+$': ''
path_sep_replace: _
art_filename: cover
plugins: []

View file

@ -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

View file

@ -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'):

View file

@ -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:

View file

@ -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):