mirror of
https://github.com/beetbox/beets.git
synced 2025-12-27 11:02:43 +01:00
query-conditioned path formats (#210)
Also, Library.path_formats is now a list of pairs instead of a dictionary. (I would have used an OrderedDict, but that was added in 2.7.)
This commit is contained in:
parent
a1b2b6c8a2
commit
d73c133a53
9 changed files with 198 additions and 115 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 </plugins/index>`. 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-------
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue