mirror of
https://github.com/beetbox/beets.git
synced 2026-01-05 23:43:31 +01:00
skip (configurable) clutter filenames when importing
This commit is contained in:
parent
02402545e0
commit
5965b37f51
11 changed files with 85 additions and 15 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ class NonAutotaggedImportTest(unittest.TestCase):
|
|||
timid = False,
|
||||
query = None,
|
||||
incremental = False,
|
||||
ignore = [],
|
||||
)
|
||||
|
||||
return paths
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue