mirror of
https://github.com/beetbox/beets.git
synced 2025-12-23 17:13:30 +01:00
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:
parent
09d6eedd6a
commit
b9cb3980c2
5 changed files with 47 additions and 38 deletions
|
|
@ -26,6 +26,7 @@ replace:
|
||||||
'[<>:"\?\*\|]': _
|
'[<>:"\?\*\|]': _
|
||||||
'\.$': _
|
'\.$': _
|
||||||
'\s+$': ''
|
'\s+$': ''
|
||||||
|
path_sep_replace: _
|
||||||
art_filename: cover
|
art_filename: cover
|
||||||
|
|
||||||
plugins: []
|
plugins: []
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from beets import util
|
||||||
from beets.util import bytestring_path, syspath, normpath, samefile,\
|
from beets.util import bytestring_path, syspath, normpath, samefile,\
|
||||||
displayable_path
|
displayable_path
|
||||||
from beets.util.functemplate import Template
|
from beets.util.functemplate import Template
|
||||||
|
import beets
|
||||||
|
|
||||||
MAX_FILENAME_LENGTH = 200
|
MAX_FILENAME_LENGTH = 200
|
||||||
|
|
||||||
|
|
@ -183,6 +184,39 @@ def _regexp(expr, val):
|
||||||
return False
|
return False
|
||||||
return res is not None
|
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.
|
# Exceptions.
|
||||||
|
|
||||||
|
|
@ -361,7 +395,7 @@ class Item(object):
|
||||||
# From Item.
|
# From Item.
|
||||||
value = getattr(self, key)
|
value = getattr(self, key)
|
||||||
if sanitize:
|
if sanitize:
|
||||||
value = util.sanitize_for_path(value, pathmod, key)
|
value = format_for_path(value, key, pathmod)
|
||||||
mapping[key] = value
|
mapping[key] = value
|
||||||
|
|
||||||
# Additional fields in non-sanitized case.
|
# Additional fields in non-sanitized case.
|
||||||
|
|
@ -378,7 +412,7 @@ class Item(object):
|
||||||
# Get values from plugins.
|
# Get values from plugins.
|
||||||
for key, value in plugins.template_values(self).iteritems():
|
for key, value in plugins.template_values(self).iteritems():
|
||||||
if sanitize:
|
if sanitize:
|
||||||
value = util.sanitize_for_path(value, pathmod, key)
|
value = format_for_path(value, key, pathmod)
|
||||||
mapping[key] = value
|
mapping[key] = value
|
||||||
|
|
||||||
# Get template functions.
|
# Get template functions.
|
||||||
|
|
@ -1568,7 +1602,7 @@ class Album(BaseAlbum):
|
||||||
if not isinstance(self._library.art_filename,Template):
|
if not isinstance(self._library.art_filename,Template):
|
||||||
self._library.art_filename = Template(self._library.art_filename)
|
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)
|
self.evaluate_template(self._library.art_filename)
|
||||||
))
|
))
|
||||||
subpath = bytestring_path(subpath)
|
subpath = bytestring_path(subpath)
|
||||||
|
|
@ -1764,8 +1798,8 @@ class DefaultTemplateFunctions(object):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# Flatten disambiguation value into a string.
|
# Flatten disambiguation value into a string.
|
||||||
disam_value = util.sanitize_for_path(getattr(album, disambiguator),
|
disam_value = format_for_path(getattr(album, disambiguator),
|
||||||
self.pathmod, disambiguator)
|
disambiguator, self.pathmod)
|
||||||
res = u' [{0}]'.format(disam_value)
|
res = u' [{0}]'.format(disam_value)
|
||||||
self.lib._memotable[memokey] = res
|
self.lib._memotable[memokey] = res
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -494,35 +494,6 @@ def truncate_path(path, pathmod=None, length=MAX_FILENAME_LENGTH):
|
||||||
|
|
||||||
return pathmod.join(*out)
|
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):
|
def str2bool(value):
|
||||||
"""Returns a boolean reflecting a human-entered string."""
|
"""Returns a boolean reflecting a human-entered string."""
|
||||||
if value.lower() in ('yes', '1', 'true', 't', 'y'):
|
if value.lower() in ('yes', '1', 'true', 't', 'y'):
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ New configuration options:
|
||||||
* :doc:`/plugins/lastgenre`: A new configuration option lets you choose to
|
* :doc:`/plugins/lastgenre`: A new configuration option lets you choose to
|
||||||
retrieve artist-level tags as genres instead of album- or track-level tags.
|
retrieve artist-level tags as genres instead of album- or track-level tags.
|
||||||
Thanks to Peter Fern and Peter Schnebel.
|
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:
|
Other new stuff:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,19 +347,19 @@ class DestinationTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_component_sanitize_replaces_separators(self):
|
def test_component_sanitize_replaces_separators(self):
|
||||||
name = posixpath.join('a', 'b')
|
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)
|
self.assertNotEqual(name, newname)
|
||||||
|
|
||||||
def test_component_sanitize_pads_with_zero(self):
|
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'))
|
self.assertTrue(name.startswith('0'))
|
||||||
|
|
||||||
def test_component_sanitize_uses_kbps_bitrate(self):
|
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')
|
self.assertEqual(val, u'12kbps')
|
||||||
|
|
||||||
def test_component_sanitize_uses_khz_samplerate(self):
|
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')
|
self.assertEqual(val, u'12kHz')
|
||||||
|
|
||||||
def test_artist_falls_back_to_albumartist(self):
|
def test_artist_falls_back_to_albumartist(self):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue