diff --git a/beets/library.py b/beets/library.py index 44fcb6b86..38f6de142 100644 --- a/beets/library.py +++ b/beets/library.py @@ -817,7 +817,7 @@ class Library(BaseLibrary): mapping['albumartist'] = mapping['artist'] # Perform substitution. - subpath = subpath_tmpl.substitute(mapping) + subpath = subpath_tmpl.substitute(mapping, TEMPLATE_FUNCTIONS) # Encode for the filesystem, dropping unencodable characters. if isinstance(subpath, unicode) and not fragment: @@ -1260,3 +1260,48 @@ class Album(BaseAlbum): util.soft_remove(oldart) util.copy(path, artdest) self.artpath = artdest + + +# Default path template resources. + +def _int_arg(s): + """Convert a string argument to an integer for use in a template + function. May raise a ValueError. + """ + return int(s.strip()) +def _tmpl_lower(s): + """Convert a string to lower case.""" + return s.lower() +def _tmpl_upper(s): + """Covert a string to upper case.""" + return s.upper() +def _tmpl_title(s): + """Convert a string to title case.""" + return s.title() +def _tmpl_left(s, chars): + """Get the leftmost characters of a string.""" + return s[0:_int_arg(chars)] +def _tmpl_right(s, chars): + """Get the rightmost characters of a string.""" + return s[-_int_arg(chars):] +def _tmpl_if(condition, trueval, falseval=u''): + """If ``condition`` is nonempty and nonzero, emit ``trueval``; + otherwise, emit ``falseval`` (if provided). + """ + try: + condition = _int_arg(condition) + except ValueError: + condition = condition.strip() + if condition: + return trueval + else: + return falseval + +TEMPLATE_FUNCTIONS = { + 'lower': _tmpl_lower, + 'upper': _tmpl_upper, + 'title': _tmpl_title, + 'left': _tmpl_left, + 'right': _tmpl_right, + 'if': _tmpl_if, +} diff --git a/test/test_db.py b/test/test_db.py index c6dbb2024..659477dd2 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -363,6 +363,60 @@ class DestinationTest(unittest.TestCase): p = util.sanitize_path('', posixpath) self.assertEqual(p, '') +class DestinationFunctionTest(unittest.TestCase): + def setUp(self): + self.lib = beets.library.Library(':memory:') + self.lib.directory = '/base' + self.lib.path_formats = {'default': u'path'} + self.i = item() + def tearDown(self): + self.lib.conn.close() + + def _setf(self, fmt): + self.lib.path_formats['default'] = fmt + def _assert_dest(self, dest): + self.assertEqual(self.lib.destination(self.i), dest) + + def test_upper_case_literal(self): + self._setf(u'%upper{foo}') + self._assert_dest('/base/FOO') + + def test_upper_case_variable(self): + self._setf(u'%upper{$title}') + self._assert_dest('/base/THE TITLE') + + def test_title_case_variable(self): + self._setf(u'%title{$title}') + self._assert_dest('/base/The Title') + + def test_left_variable(self): + self._setf(u'%left{$title, 3}') + self._assert_dest('/base/the') + + def test_right_variable(self): + self._setf(u'%right{$title,3}') + self._assert_dest('/base/tle') + + def test_if_false(self): + self._setf(u'%if{,foo}') + self._assert_dest('/base/') + + def test_if_true(self): + self._setf(u'%if{bar,foo}') + self._assert_dest('/base/foo') + + def test_if_else_false(self): + self._setf(u'%if{,foo,baz}') + self._assert_dest('/base/baz') + + def test_if_int_value(self): + self._setf(u'%if{0,foo,baz}') + self._assert_dest('/base/baz') + + def test_nonexistent_function(self): + self._setf(u'%foo{bar}') + self._assert_dest('/base/%foo{bar}') + class MigrationTest(unittest.TestCase): """Tests the ability to change the database schema between versions.