diff --git a/beets/library.py b/beets/library.py index ac890f560..64f67b511 100644 --- a/beets/library.py +++ b/beets/library.py @@ -17,6 +17,7 @@ import os import re import sys import logging +import shlex from unidecode import unidecode from beets.mediafile import MediaFile from beets import plugins @@ -103,6 +104,9 @@ ALBUM_DEFAULT_FIELDS = ('album', 'albumartist', 'genre') ITEM_DEFAULT_FIELDS = ARTIST_DEFAULT_FIELDS + ALBUM_DEFAULT_FIELDS + \ ('title', 'comments') +# Special path format key. +PF_KEY_DEFAULT = 'default' + # Logger. log = logging.getLogger('beets') if not log.handlers: @@ -350,8 +354,8 @@ class CollectionQuery(Query): # is there a better way to do this? def __len__(self): return len(self.subqueries) def __getitem__(self, key): return self.subqueries[key] - def __iter__(self): iter(self.subqueries) - def __contains__(self, item): item in self.subqueries + def __iter__(self): return iter(self.subqueries) + def __contains__(self, item): return item in self.subqueries def clause_with_joiner(self, joiner): """Returns a clause created by joining together the clauses of @@ -425,6 +429,13 @@ class CollectionQuery(Query): subqueries = [TrueQuery()] return cls(subqueries) + @classmethod + def from_string(cls, query, default_fields=None, all_keys=ITEM_KEYS): + """Creates a query based on a single string. The string is split + into query parts using shell-style syntax. + """ + return cls.from_strings(shlex.split(query)) + class AnySubstringQuery(CollectionQuery): """A query that matches a substring in any of a list of metadata fields. @@ -479,6 +490,14 @@ class TrueQuery(Query): def match(self, item): return True +class FalseQuery(Query): + """A query that never matches.""" + def clause(self): + return '0', () + + def match(self, item): + return False + class PathQuery(Query): """A query that matches all items under a given path.""" def __init__(self, path): @@ -702,7 +721,8 @@ class Library(BaseLibrary): """A music library using an SQLite database as a metadata store.""" def __init__(self, path='library.blb', directory='~/Music', - path_formats=None, + path_formats=((PF_KEY_DEFAULT, + '$artist/$album/$track $title'),), art_filename='cover', timeout=5.0, replacements=None, @@ -713,10 +733,6 @@ class Library(BaseLibrary): else: self.path = bytestring_path(normpath(path)) self.directory = bytestring_path(normpath(directory)) - if path_formats is None: - path_formats = {'default': '$artist/$album/$track $title'} - elif isinstance(path_formats, basestring): - path_formats = {'default': path_formats} self.path_formats = path_formats self.art_filename = bytestring_path(art_filename) self.replacements = replacements @@ -784,19 +800,31 @@ class Library(BaseLibrary): """ pathmod = pathmod or os.path - # Use a path format based on the album type, if available. - if not item.album_id and not in_album: - # Singleton track. Never use the "album" formats. - if 'singleton' in self.path_formats: - path_format = self.path_formats['singleton'] - else: - path_format = self.path_formats['default'] - elif item.albumtype and item.albumtype in self.path_formats: - path_format = self.path_formats[item.albumtype] - elif item.comp and 'comp' in self.path_formats: - path_format = self.path_formats['comp'] + # Use a path format based on a query, falling back on the + # default. + for query, path_format in self.path_formats: + if query == PF_KEY_DEFAULT: + continue + query = AndQuery.from_string(query) + if in_album: + # If we're treating this item as a member of the item, + # hack the query so that singleton queries always + # observe the item to be non-singleton. + for i, subquery in enumerate(query): + if isinstance(subquery, SingletonQuery): + query[i] = FalseQuery() if subquery.sense \ + else TrueQuery() + if query.match(item): + # The query matches the item! Use the corresponding path + # format. + break else: - path_format = self.path_formats['default'] + # No query matched; fall back to default. + for query, path_format in self.path_formats: + if query == PF_KEY_DEFAULT: + break + else: + assert False, "no default path format" subpath_tmpl = Template(path_format) # Get the item's Album if it has one. diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 30522fe9d..022fc00d4 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -40,15 +40,18 @@ DEFAULT_LIBRARY_FILENAME_UNIX = '.beetsmusic.blb' DEFAULT_LIBRARY_FILENAME_WINDOWS = 'beetsmusic.blb' DEFAULT_DIRECTORY_NAME = 'Music' WINDOWS_BASEDIR = os.environ.get('APPDATA') or '~' -DEFAULT_PATH_FORMATS = { - 'default': '$albumartist/$album/$track $title', - 'comp': 'Compilations/$album/$track $title', - 'singleton': 'Non-Album/$artist/$title', +PF_KEY_QUERIES = { + 'comp': 'comp:true', + 'singleton': 'singleton:true', } +DEFAULT_PATH_FORMATS = [ + (library.PF_KEY_DEFAULT, '$albumartist/$album/$track $title'), + (PF_KEY_QUERIES['singleton'], 'Non-Album/$artist/$title'), + (PF_KEY_QUERIES['comp'], 'Compilations/$album/$track $title'), +] DEFAULT_ART_FILENAME = 'cover' DEFAULT_TIMEOUT = 5.0 - # UI exception. Commands should throw this in order to display # nonrecoverable errors to the user. class UserError(Exception): @@ -429,6 +432,31 @@ def _get_replacements(config): out.append((re.compile(pattern), replacement)) return out +def _get_path_formats(config): + """Returns a list of path formats (query/template pairs); reflecting + the config's specified path formats. + """ + legacy_path_format = config_val(config, 'beets', 'path_format', None) + if legacy_path_format: + # Old path formats override the default values. + path_formats = [(library.PF_KEY_DEFAULT, legacy_path_format)] + else: + # If no legacy path format, use the defaults instead. + path_formats = DEFAULT_PATH_FORMATS + if config.has_section('paths'): + custom_path_formats = [] + for key, value in config.items('paths', True): + if key in PF_KEY_QUERIES: + # Special values that indicate simple queries. + key = PF_KEY_QUERIES[key] + elif key != library.PF_KEY_DEFAULT: + # For non-special keys (literal queries), the _ + # character denotes a :. + key = key.replace('_', ':') + custom_path_formats.append((key, value)) + path_formats = custom_path_formats + path_formats + return path_formats + # Subcommand parsing infrastructure. @@ -648,15 +676,7 @@ def main(args=None, configfh=None): config_val(config, 'beets', 'library', default_libpath) directory = options.directory or \ config_val(config, 'beets', 'directory', default_dir) - legacy_path_format = config_val(config, 'beets', 'path_format', None) - if legacy_path_format: - # Old path formats override the default values. - path_formats = {'default': legacy_path_format} - else: - # If no legacy path format, use the defaults instead. - path_formats = DEFAULT_PATH_FORMATS - if config.has_section('paths'): - path_formats.update(config.items('paths', True)) + path_formats = _get_path_formats(config) art_filename = \ config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME) lib_timeout = config_val(config, 'beets', 'timeout', DEFAULT_TIMEOUT) diff --git a/docs/changelog.rst b/docs/changelog.rst index e0d1c4183..4bcd17d57 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,12 @@ This release focuses on making beets' path formatting vastly more powerful. * Plugins can also now define new path *fields* in addition to functions. * The new :doc:`/plugins/inline` lets you **use Python expressions to customize path formats** by defining new fields in the config file. +* The configuration can **condition path formats based on queries**. That is, + you can write a path format that is only used if an item matches a given + query. (This supersedes the earlier functionality that only allowed + conditioning on album type; if you used this feature in a previous version, + you will need to replace, for example, ``soundtrack:`` with + ``albumtype_soundtrack:``.) See :ref:`path-format-config`. * **Filename substitutions are now configurable** via the ``replace`` config value. You can choose which characters you think should be allowed in your directory and music file names. See :doc:`/reference/config`. @@ -287,7 +293,7 @@ that functionality. Tracks" (T) option to add singletons to your library. To list only singleton or only album tracks, use the new ``singleton:`` query term: the query ``singleton:true`` matches only singleton tracks; ``singleton:false`` matches - only album tracks. The :doc:`/plugins/lastid` has been extended to support + only album tracks. The ``lastid`` plugin has been extended to support matching individual items as well. * The importer/autotagger system has been heavily refactored in this release. @@ -474,7 +480,7 @@ it more reliable. This release also greatly expands the capabilities of beets' :doc:`plugin API `. A host of other little features and fixes are also rolled into this release. -* The :doc:`/plugins/lastid` adds Last.fm **acoustic fingerprinting +* The ``lastid`` plugin adds Last.fm **acoustic fingerprinting support** to the autotagger. Similar to the PUIDs used by !MusicBrainz Picard, this system allows beets to recognize files that don't have any metadata at all. You'll need to install some dependencies for this plugin to work. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 8caf0be68..34c2572c3 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -130,31 +130,40 @@ section header: exception when the database lock is contended. This should almost never need to be changed except on very slow systems. Defaults to 5.0 (5 seconds). -Path Formats ------------- +.. _path-format-config: + +Path Format Configuration +------------------------- You can also configure the directory hierarchy beets uses to store music. These settings appear under the ``[paths]`` section (rather than the main ``[beets]`` -section we used above). Each string is a `template string`_ that can refer to +section we used above). Each string is a template string that can refer to metadata fields like ``$artist`` or ``$title``. The filename extension is added automatically. At the moment, you can specify three special paths: ``default`` for most releases, ``comp`` for "various artist" releases with no dominant -artist, and ``singleton`` for non-album tracks. You can also specify a different -path format for each `MusicBrainz release type`_. The defaults look like this:: +artist, and ``singleton`` for non-album tracks. The defaults look like this:: [paths] default: $albumartist/$album/$track $title - comp: Compilations/$album/$track title singleton: Non-Album/$artist/$title + comp: Compilations/$album/$track title Note the use of ``$albumartist`` instead of ``$artist``; this ensure that albums will be well-organized. For more about these format strings, see :doc:`pathformat`. -.. _template string: - http://docs.python.org/library/string.html#template-strings -.. _MusicBrainz release type: - http://wiki.musicbrainz.org/ReleaseType +In addition to ``default``, ``comp``, and ``singleton``, you can condition path +queries based on beets queries (see :doc:`/reference/query`). There's one catch: +because the ``:`` character is reserved for separating the query from the +template string, the ``_`` character is substituted for ``:`` in these queries. +This means that a config file like this:: + + [paths] + albumtype_soundtrack: Soundtracks/$albumartist/$track title + +will place soundtrack albums in a separate directory. The queries are tested in +the order they appear in the configuration file, meaning that if an item matches +multiple queries, beets will use the path format for the *first* matching query. Example ------- diff --git a/test/test_db.py b/test/test_db.py index e40ad5283..27b51d84e 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -138,17 +138,17 @@ class DestinationTest(unittest.TestCase): def test_directory_works_with_trailing_slash(self): self.lib.directory = 'one/' - self.lib.path_formats = {'default': 'two'} + self.lib.path_formats = [('default', 'two')] self.assertEqual(self.lib.destination(self.i), np('one/two')) def test_directory_works_without_trailing_slash(self): self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two'} + self.lib.path_formats = [('default', 'two')] self.assertEqual(self.lib.destination(self.i), np('one/two')) def test_destination_substitues_metadata_values(self): self.lib.directory = 'base' - self.lib.path_formats = {'default': '$album/$artist $title'} + self.lib.path_formats = [('default', '$album/$artist $title')] self.i.title = 'three' self.i.artist = 'two' self.i.album = 'one' @@ -157,15 +157,15 @@ class DestinationTest(unittest.TestCase): def test_destination_preserves_extension(self): self.lib.directory = 'base' - self.lib.path_formats = {'default': '$title'} + self.lib.path_formats = [('default', '$title')] self.i.path = 'hey.audioFormat' self.assertEqual(self.lib.destination(self.i), np('base/the title.audioFormat')) def test_destination_pads_some_indices(self): self.lib.directory = 'base' - self.lib.path_formats = {'default': '$track $tracktotal ' \ - '$disc $disctotal $bpm'} + self.lib.path_formats = [('default', '$track $tracktotal ' \ + '$disc $disctotal $bpm')] self.i.track = 1 self.i.tracktotal = 2 self.i.disc = 3 @@ -176,7 +176,7 @@ class DestinationTest(unittest.TestCase): def test_destination_pads_date_values(self): self.lib.directory = 'base' - self.lib.path_formats = {'default': '$year-$month-$day'} + self.lib.path_formats = [('default', '$year-$month-$day')] self.i.year = 1 self.i.month = 2 self.i.day = 3 @@ -245,7 +245,7 @@ class DestinationTest(unittest.TestCase): self.assertEqual(p, u'-') def test_path_with_format(self): - self.lib.path_formats = {'default': '$artist/$album ($format)'} + self.lib.path_formats = [('default', '$artist/$album ($format)')] p = self.lib.destination(self.i) self.assert_('(FLAC)' in p) @@ -253,7 +253,7 @@ class DestinationTest(unittest.TestCase): i1, i2 = item(), item() self.lib.add_album([i1, i2]) i1.year, i2.year = 2009, 2010 - self.lib.path_formats = {'default': '$album ($year)/$track $title'} + self.lib.path_formats = [('default', '$album ($year)/$track $title')] dest1, dest2 = self.lib.destination(i1), self.lib.destination(i2) self.assertEqual(os.path.dirname(dest1), os.path.dirname(dest2)) @@ -261,44 +261,51 @@ class DestinationTest(unittest.TestCase): self.i.comp = False self.lib.add_album([self.i]) self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two', - 'comp': 'three'} + self.lib.path_formats = [('default', 'two'), + ('comp:true', 'three')] self.assertEqual(self.lib.destination(self.i), np('one/two')) def test_singleton_path(self): i = item() self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two', - 'comp': 'three', - 'singleton': 'four'} + self.lib.path_formats = [ + ('default', 'two'), + ('singleton:true', 'four'), + ('comp:true', 'three'), + ] self.assertEqual(self.lib.destination(i), np('one/four')) - def test_singleton_track_falls_back_to_default(self): + def test_comp_before_singleton_path(self): i = item() i.comp = True - i.albumtype = 'atype' self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two', - 'comp': 'three', - 'atype': 'four'} - self.assertEqual(self.lib.destination(i), np('one/two')) + self.lib.path_formats = [ + ('default', 'two'), + ('comp:true', 'three'), + ('singleton:true', 'four'), + ] + self.assertEqual(self.lib.destination(i), np('one/three')) def test_comp_path(self): self.i.comp = True self.lib.add_album([self.i]) self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two', - 'comp': 'three'} + self.lib.path_formats = [ + ('default', 'two'), + ('comp:true', 'three'), + ] self.assertEqual(self.lib.destination(self.i), np('one/three')) - def test_albumtype_path(self): + def test_albumtype_query_path(self): self.i.comp = True self.lib.add_album([self.i]) self.i.albumtype = 'sometype' self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two', - 'comp': 'three', - 'sometype': 'four'} + self.lib.path_formats = [ + ('default', 'two'), + ('albumtype:sometype', 'four'), + ('comp:true', 'three'), + ] self.assertEqual(self.lib.destination(self.i), np('one/four')) def test_albumtype_path_fallback_to_comp(self): @@ -306,9 +313,11 @@ class DestinationTest(unittest.TestCase): self.lib.add_album([self.i]) self.i.albumtype = 'sometype' self.lib.directory = 'one' - self.lib.path_formats = {'default': 'two', - 'comp': 'three', - 'anothertype': 'four'} + self.lib.path_formats = [ + ('default', 'two'), + ('albumtype:anothertype', 'four'), + ('comp:true', 'three'), + ] self.assertEqual(self.lib.destination(self.i), np('one/three')) def test_syspath_windows_format(self): @@ -342,28 +351,28 @@ class DestinationTest(unittest.TestCase): def test_artist_falls_back_to_albumartist(self): self.i.artist = '' self.i.albumartist = 'something' - self.lib.path_formats = {'default': '$artist'} + self.lib.path_formats = [('default', '$artist')] p = self.lib.destination(self.i) self.assertEqual(p.rsplit(os.path.sep, 1)[1], 'something') def test_albumartist_falls_back_to_artist(self): self.i.artist = 'trackartist' self.i.albumartist = '' - self.lib.path_formats = {'default': '$albumartist'} + self.lib.path_formats = [('default', '$albumartist')] p = self.lib.destination(self.i) self.assertEqual(p.rsplit(os.path.sep, 1)[1], 'trackartist') def test_artist_overrides_albumartist(self): self.i.artist = 'theartist' self.i.albumartist = 'something' - self.lib.path_formats = {'default': '$artist'} + self.lib.path_formats = [('default', '$artist')] p = self.lib.destination(self.i) self.assertEqual(p.rsplit(os.path.sep, 1)[1], 'theartist') def test_albumartist_overrides_artist(self): self.i.artist = 'theartist' self.i.albumartist = 'something' - self.lib.path_formats = {'default': '$albumartist'} + self.lib.path_formats = [('default', '$albumartist')] p = self.lib.destination(self.i) self.assertEqual(p.rsplit(os.path.sep, 1)[1], 'something') @@ -387,13 +396,13 @@ 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.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 + self.lib.path_formats.insert(0, ('default', fmt)) def _assert_dest(self, dest): self.assertEqual(self.lib.destination(self.i), dest) diff --git a/test/test_files.py b/test/test_files.py index 188d9be56..29ba2fb6a 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -40,7 +40,8 @@ class MoveTest(unittest.TestCase, _common.ExtraAsserts): # set up the destination self.libdir = join(_common.RSRC, 'testlibdir') self.lib.directory = self.libdir - self.lib.path_formats = {'default': join('$artist', '$album', '$title')} + self.lib.path_formats = [('default', + join('$artist', '$album', '$title'))] self.i.artist = 'one' self.i.album = 'two' self.i.title = 'three' @@ -157,7 +158,7 @@ class AlbumFileTest(unittest.TestCase): # Make library and item. self.lib = beets.library.Library(':memory:') self.lib.path_formats = \ - {'default': join('$albumartist', '$album', '$title')} + [('default', join('$albumartist', '$album', '$title'))] self.libdir = os.path.join(_common.RSRC, 'testlibdir') self.lib.directory = self.libdir self.i = item() diff --git a/test/test_importer.py b/test/test_importer.py index f87189b56..3f9ee2029 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -34,9 +34,9 @@ class NonAutotaggedImportTest(unittest.TestCase): self.lib = library.Library(self.libdb) self.libdir = os.path.join(_common.RSRC, 'testlibdir') self.lib.directory = self.libdir - self.lib.path_formats = { - 'default': os.path.join('$artist', '$album', '$title') - } + self.lib.path_formats = [( + 'default', os.path.join('$artist', '$album', '$title') + )] self.srcdir = os.path.join(_common.RSRC, 'testsrcdir') @@ -164,11 +164,11 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): self.libdir = os.path.join(_common.RSRC, 'testlibdir') os.mkdir(self.libdir) self.lib = library.Library(':memory:', self.libdir) - self.lib.path_formats = { - 'default': 'one', - 'comp': 'two', - 'singleton': 'three', - } + self.lib.path_formats = [ + ('default', 'one'), + ('singleton:true', 'three'), + ('comp:true', 'two'), + ] self.srcpath = os.path.join(self.libdir, 'srcfile.mp3') shutil.copy(os.path.join(_common.RSRC, 'full.mp3'), self.srcpath) @@ -212,17 +212,13 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): coro = importer.apply_choices(_common.iconfig(self.lib)) coro.next() # Prime coroutine. _call_apply_choice(coro, [self.i], importer.action.ASIS) - self.assertExists( - os.path.join(self.libdir, self.lib.path_formats['default']+'.mp3') - ) + self.assertExists(os.path.join(self.libdir, 'one.mp3')) def test_apply_match_uses_album_path(self): coro = importer.apply_choices(_common.iconfig(self.lib)) coro.next() # Prime coroutine. _call_apply(coro, [self.i], self.info) - self.assertExists( - os.path.join(self.libdir, self.lib.path_formats['default']+'.mp3') - ) + self.assertExists(os.path.join(self.libdir, 'one.mp3')) def test_apply_tracks_uses_singleton_path(self): coro = importer.apply_choices(_common.iconfig(self.lib)) @@ -233,7 +229,7 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): coro.send(task) self.assertExists( - os.path.join(self.libdir, self.lib.path_formats['singleton']+'.mp3') + os.path.join(self.libdir, 'three.mp3') ) def test_apply_sentinel(self): @@ -296,9 +292,9 @@ class ApplyExistingItemsTest(unittest.TestCase, _common.ExtraAsserts): self.dbpath = os.path.join(_common.RSRC, 'templib.blb') self.lib = library.Library(self.dbpath, self.libdir) - self.lib.path_formats = { - 'default': '$artist/$title', - } + self.lib.path_formats = [ + ('default', '$artist/$title'), + ] self.config = _common.iconfig(self.lib, write=False, copy=False) self.srcpath = os.path.join(self.libdir, 'srcfile.mp3') diff --git a/test/test_ui.py b/test/test_ui.py index 23db3100e..ece9a41c3 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -21,6 +21,7 @@ import textwrap import logging import re from StringIO import StringIO +import ConfigParser import _common from beets import library @@ -478,27 +479,19 @@ class ConfigTest(unittest.TestCase): def test_paths_section_respected(self): def func(lib, config, opts, args): - self.assertEqual(lib.path_formats['x'], 'y') + self.assertEqual(lib.path_formats[0], ('x', 'y')) self._run_main([], textwrap.dedent(""" [paths] x=y"""), func) def test_default_paths_preserved(self): def func(lib, config, opts, args): - self.assertEqual(lib.path_formats['default'], - ui.DEFAULT_PATH_FORMATS['default']) + self.assertEqual(lib.path_formats[1:], + ui.DEFAULT_PATH_FORMATS) self._run_main([], textwrap.dedent(""" [paths] x=y"""), func) - def test_default_paths_overriden_by_legacy_path_format(self): - def func(lib, config, opts, args): - self.assertEqual(lib.path_formats['default'], 'x') - self.assertEqual(len(lib.path_formats), 1) - self._run_main([], textwrap.dedent(""" - [beets] - path_format=x"""), func) - def test_nonexistant_config_file(self): os.environ['BEETSCONFIG'] = '/xxxxx' ui.main(['version']) @@ -718,6 +711,27 @@ class DefaultPathTest(unittest.TestCase): self.assertEqual(lib, 'xappdata\\beetsmusic.blb') self.assertEqual(libdir, 'xhome\\Music') +class PathFormatTest(unittest.TestCase): + def _config(self, text): + cp = ConfigParser.SafeConfigParser() + cp.readfp(StringIO(text)) + return cp + + def _paths_for(self, text): + return ui._get_path_formats(self._config("[paths]\n%s" % + textwrap.dedent(text))) + + def test_default_paths(self): + pf = self._paths_for("") + self.assertEqual(pf, ui.DEFAULT_PATH_FORMATS) + + def test_custom_paths_prepend(self): + pf = self._paths_for(""" + foo: bar + """) + self.assertEqual(pf[0], ('foo', 'bar')) + self.assertEqual(pf[1:], ui.DEFAULT_PATH_FORMATS) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/test/test_vfs.py b/test/test_vfs.py index 51fe8231b..526eec800 100644 --- a/test/test_vfs.py +++ b/test/test_vfs.py @@ -21,10 +21,10 @@ from beets import vfs class VFSTest(unittest.TestCase): def setUp(self): - self.lib = library.Library(':memory:', path_formats={ - 'default': 'albums/$album/$title', - 'singleton': 'tracks/$artist/$title', - }) + self.lib = library.Library(':memory:', path_formats=[ + ('default', 'albums/$album/$title'), + ('singleton:true', 'tracks/$artist/$title'), + ]) self.lib.add(_common.item()) self.lib.add_album([_common.item()]) self.lib.save()