skip (configurable) clutter filenames when importing

This commit is contained in:
Adrian Sampson 2011-11-13 17:14:40 -08:00
parent 02402545e0
commit 5965b37f51
11 changed files with 85 additions and 15 deletions

View file

@ -33,13 +33,14 @@ log = logging.getLogger('beets')
# Additional utilities for the main interface.
def albums_in_dir(path):
def albums_in_dir(path, ignore=()):
"""Recursively searches the given directory and returns an iterable
of (path, items) where path is a containing directory and items is
a list of Items that is probably an album. Specifically, any folder
containing any media files is an album.
containing any media files is an album. Directories and file names
that match the glob patterns in ``ignore`` are skipped.
"""
for root, dirs, files in sorted_walk(path):
for root, dirs, files in sorted_walk(path, ignore):
# Get a list of items in the directory.
items = []
for filename in files:

View file

@ -265,7 +265,7 @@ class ImportConfig(object):
'quiet_fallback', 'copy', 'write', 'art', 'delete',
'choose_match_func', 'should_resume_func', 'threaded',
'autot', 'singletons', 'timid', 'choose_item_func',
'query', 'incremental']
'query', 'incremental', 'ignore']
def __init__(self, **kwargs):
for slot in self._fields:
setattr(self, slot, kwargs[slot])
@ -455,7 +455,7 @@ def read_tasks(config):
# Produce paths under this directory.
if progress:
resume_dir = resume_dirs.get(toppath)
for path, items in autotag.albums_in_dir(toppath):
for path, items in autotag.albums_in_dir(toppath, config.ignore):
# Skip according to progress.
if progress and resume_dir:
# We're fast-forwarding to resume a previous tagging.

View file

@ -257,10 +257,10 @@ def input_yn(prompt, require=False, color=False):
return sel == 'y'
def config_val(config, section, name, default, vtype=None):
"""Queries the configuration file for a value (given by the
section and name). If no value is present, returns default.
vtype optionally specifies the return type (although only bool
is supported for now).
"""Queries the configuration file for a value (given by the section
and name). If no value is present, returns default. vtype
optionally specifies the return type (although only ``bool`` and
``list`` are supported for now).
"""
if not config.has_section(section):
config.add_section(section)
@ -268,6 +268,10 @@ def config_val(config, section, name, default, vtype=None):
try:
if vtype is bool:
return config.getboolean(section, name)
elif vtype is list:
# Whitespace-separated strings.
strval = config.get(section, name)
return strval.split()
else:
return config.get(section, name)
except ConfigParser.NoOptionError:

View file

@ -97,6 +97,7 @@ DEFAULT_IMPORT_RESUME = None # "ask"
DEFAULT_IMPORT_INCREMENTAL = False
DEFAULT_THREADED = True
DEFAULT_COLOR = True
DEFAULT_IGNORE = ['.AppleDouble', '._*', '*~', '.DS_Store']
VARIOUS_ARTISTS = u'Various Artists'
@ -506,7 +507,7 @@ def choose_item(task, config):
def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
color, delete, quiet, resume, quiet_fallback, singletons,
timid, query, incremental):
timid, query, incremental, ignore):
"""Import the files in the given list of paths, tagging each leaf
directory as an album. If copy, then the files are copied into
the library folder. If write, then new metadata is written to the
@ -568,6 +569,7 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
choose_item_func = choose_item,
query = query,
incremental = incremental,
ignore = ignore,
)
# If we were logging, close the file.
@ -641,6 +643,7 @@ def import_func(lib, config, opts, args):
incremental = opts.incremental if opts.incremental is not None else \
ui.config_val(config, 'beets', 'import_incremental',
DEFAULT_IMPORT_INCREMENTAL, bool)
ignore = ui.config_val(config, 'beets', 'ignore', DEFAULT_IGNORE, list)
# Resume has three options: yes, no, and "ask" (None).
resume = opts.resume if opts.resume is not None else \
@ -667,7 +670,7 @@ def import_func(lib, config, opts, args):
import_files(lib, paths, copy, write, autot, logpath, art, threaded,
color, delete, quiet, resume, quiet_fallback, singletons,
timid, query, incremental)
timid, query, incremental, ignore)
import_cmd.func = import_func
default_commands.append(import_cmd)

View file

@ -17,6 +17,7 @@ import os
import sys
import re
import shutil
import fnmatch
from collections import defaultdict
MAX_FILENAME_LENGTH = 200
@ -47,9 +48,10 @@ def ancestry(path, pathmod=None):
out.insert(0, path)
return out
def sorted_walk(path):
"""Like os.walk, but yields things in sorted, breadth-first
order.
def sorted_walk(path, ignore=()):
"""Like ``os.walk``, but yields things in sorted, breadth-first
order. Directory and file names matching any glob pattern in
``ignore`` are skipped.
"""
# Make sure the path isn't a Unicode string.
path = bytestring_path(path)
@ -58,6 +60,16 @@ def sorted_walk(path):
dirs = []
files = []
for base in os.listdir(path):
# Skip ignored filenames.
skip = False
for pat in ignore:
if fnmatch.fnmatch(base, pat):
skip = True
break
if skip:
continue
# Add to output as either a file or a directory.
cur = os.path.join(path, base)
if os.path.isdir(syspath(cur)):
dirs.append(base)

View file

@ -23,6 +23,9 @@ Changelog
* When entering an ID manually during tagging, beets now searches for anything
that looks like an MBID in the entered string. This means that full
MusicBrainz URLs now work as IDs at the prompt. (Thanks to derwin.)
* The importer now ignores certain "clutter" files like ``.AppleDouble``
directories and ``._*`` files. The list of ignored patterns is configurable
via the ``ignore`` setting; see :doc:`/reference/config`.
* Fix a crash after using the "as Tracks" option during import.
* Fix a Unicode error when tagging items with missing titles.

View file

@ -53,6 +53,10 @@ must appear under the ``[beets]`` section header:
By default, no log is written. This can be overridden with the ``-l`` flag to
``import``.
* ``ignore``: a space-separated list of glob patterns specifying file and
directory names to be ignored when importing. Defaults to
``.AppleDouble ._* *~ .DS_Store``.
* ``art_filename``: when importing album art, the name of the file (without
extension) where the cover art image should be placed. Defaults to ``cover``
(i.e., images will be named ``cover.jpg`` or ``cover.png`` and placed in the
@ -109,6 +113,7 @@ Here's an example file::
import_quiet_fallback: skip
import_timid: no
import_log: beetslog.txt
ignore: .AppleDouble ._* *~ .DS_Store
art_filename: albumart
plugins: bpd
pluginpath: ~/beets/myplugins

View file

@ -88,6 +88,7 @@ def iconfig(lib, **kwargs):
timid = False,
query = None,
incremental = False,
ignore = [],
)
for k, v in kwargs.items():
setattr(config, k, v)

View file

@ -493,6 +493,46 @@ class PruneTest(unittest.TestCase, _common.ExtraAsserts):
self.assertExists(self.base)
self.assertNotExists(self.sub)
class WalkTest(unittest.TestCase):
def setUp(self):
self.base = os.path.join(_common.RSRC, 'testdir')
os.mkdir(self.base)
touch(os.path.join(self.base, 'y'))
touch(os.path.join(self.base, 'x'))
os.mkdir(os.path.join(self.base, 'd'))
touch(os.path.join(self.base, 'd', 'z'))
def tearDown(self):
if os.path.exists(self.base):
shutil.rmtree(self.base)
def test_sorted_files(self):
res = list(util.sorted_walk(self.base))
self.assertEqual(len(res), 2)
self.assertEqual(res[0],
(self.base, ['d'], ['x', 'y']))
self.assertEqual(res[1],
(os.path.join(self.base, 'd'), [], ['z']))
def test_ignore_file(self):
res = list(util.sorted_walk(self.base, ('x',)))
self.assertEqual(len(res), 2)
self.assertEqual(res[0],
(self.base, ['d'], ['y']))
self.assertEqual(res[1],
(os.path.join(self.base, 'd'), [], ['z']))
def test_ignore_directory(self):
res = list(util.sorted_walk(self.base, ('d',)))
self.assertEqual(len(res), 1)
self.assertEqual(res[0],
(self.base, [], ['x', 'y']))
def test_ignore_everything(self):
res = list(util.sorted_walk(self.base, ('*',)))
self.assertEqual(len(res), 1)
self.assertEqual(res[0],
(self.base, [], []))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)

View file

@ -103,6 +103,7 @@ class NonAutotaggedImportTest(unittest.TestCase):
timid = False,
query = None,
incremental = False,
ignore = [],
)
return paths

View file

@ -431,7 +431,7 @@ class ImportTest(unittest.TestCase):
self.assertRaises(ui.UserError, commands.import_files,
None, [], False, False, False, None, False, False,
False, False, True, False, None, False, True, None,
False)
False, [])
class InputTest(unittest.TestCase):
def setUp(self):