diff --git a/beets/attachments.py b/beets/attachments.py index 84a5fa7a5..3a9cc9d01 100644 --- a/beets/attachments.py +++ b/beets/attachments.py @@ -18,6 +18,7 @@ import os.path import collections import logging from argparse import ArgumentParser +from fnmatch import fnmatch from beets import dbcore from beets.dbcore.query import Query, AndQuery, MatchQuery, OrQuery, FalseQuery @@ -457,7 +458,7 @@ class AttachmentFactory(object): return self.find(queries[Attachment], queries[Album], queries[Item]) def discover(self, entity_or_prefix, local=None): - """Return a list of non-audio files whose path start with the + """Return a list of non-audio file paths that start with the entity prefix. For albums the entity prefix is the album directory. For items it @@ -469,6 +470,9 @@ class AttachmentFactory(object): type. For albums the only separator is the directory separator. For items the separtors are configured by `attachments.item_sep` """ + # TODO return attachments with create() + # FIXME we need to handle paths as `entity_prefix` because of + # the importer. prefix, dir = self.path_prefix(entity_or_prefix) if local is None: return self._discover_full(prefix, dir) @@ -507,7 +511,11 @@ class AttachmentFactory(object): a list of types for `path`. For each type it yields an attachment through `create`. """ - for type in self._detect_types(path): + # TODO entity should not be optional + # TODO update doc + types = self._detect_plugin_types(path) + types.update(self._detect_config_types(path)) + for type in types: yield self.create(path, type, entity) @classmethod @@ -599,27 +607,33 @@ class AttachmentFactory(object): if hasattr(plugin, 'attachment_collector'): self.register_collector(plugin.attachment_collector) - def _detect_types(self, path): - """Yield a list of types registered for the path. - - Uses the functions from `register_detector` and the - `attachments.types` configuration. - """ + def _detect_plugin_types(self, path): + types = set() # TODO Make list unique for detector in self._detectors: try: type = detector(path) if type: - yield type + types.add(type) except: # TODO logging? pass + return types + def _detect_config_types(self, path): + types = set() types_config = config('types') - if types_config.exists(): - for matcher, type in types_config.get(dict).items(): - if re.match(matcher, path): - yield type + if not types_config.exists(): + return types + + basename = os.path.basename(path) + for pattern, type in types_config.get(dict).items(): + if ((pattern[0] == '/' and pattern[-1] == '/' + and re.match(pattern[1:-1] + '$', basename)) + or fnmatch(basename, pattern)): + types.add(type) + + return types def _collect_meta(self, type, path): all_meta = {} diff --git a/beets/ui/commands.py b/beets/ui/commands.py index e835511a8..262f2753b 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -80,8 +80,6 @@ def _do_query(lib, query, album, also_items=True): class AttachCommand(ui.Subcommand): - """Duck type for ui.Subcommand - """ def __init__(self): super(AttachCommand, self).__init__( @@ -172,12 +170,37 @@ class AttachListCommand(ui.Subcommand): args = decargs(args) factory = AttachmentFactory(lib) for a in factory.parse_and_find(*args): - print('{0}: {1}'.format(a.type, displayable_path(a.path))) + print(u'{0}: {1}'.format(a.type, displayable_path(a.path))) default_commands.append(AttachListCommand()) +class AttachImportCommand(ui.Subcommand): + """Search files in album directories and create attachments for them. + """ + + def __init__(self): + super(AttachImportCommand, self).__init__( + 'attach-import', + help='create attachments for albums already in the library' + ) + + def func(self, lib, opts, args): + args = decargs(args) + factory = AttachmentFactory(lib) + for album in lib.albums(decargs(args)): + for path in factory.discover(album): + for attachment in factory.detect(path, album): + print(u"add {0} attachment {1} to '{2} - {3}'" + .format(attachment.type, path, + album.albumartist, album.album)) + attachment.add() + + +default_commands.append(AttachImportCommand()) + + # fields: Shows a list of available fields for queries and format strings. def fields_func(lib, opts, args): diff --git a/test/helper.py b/test/helper.py index 8ae5245fe..5f409d6da 100644 --- a/test/helper.py +++ b/test/helper.py @@ -81,13 +81,13 @@ def capture_stdout(): 'spam' """ org = sys.stdout - sys.stdout = StringIO() + sys.stdout = captured = StringIO() sys.stdout.encoding = 'utf8' try: - yield sys.stdout + yield captured finally: sys.stdout = org - print(org.getvalue()) + print(captured.getvalue()) @contextmanager diff --git a/test/test_attachments.py b/test/test_attachments.py index 4c917cdfd..95b6de961 100644 --- a/test/test_attachments.py +++ b/test/test_attachments.py @@ -197,7 +197,24 @@ class AttachmentDocTest(unittest.TestCase, AttachmentTestHelper): self.assertIn("add booklet attachment {0} to 'Artist - Album 0'" .format(booklet_path), output) - # TODO attach-import + def test_attach_import(self): + self.config['attachments']['types'] = {'cover.jpg': 'cover'} + + album1 = self.add_album(name='Revolver', artist='The Beatles') + album2 = self.add_album(name='Abbey Road', artist='The Beatles') + cover_path1 = self.touch(os.path.join(album1.item_dir(), 'cover.jpg')) + cover_path2 = self.touch(os.path.join(album2.item_dir(), 'cover.jpg')) + + output = self.cli_output('attach-import') + self.assertIn("add cover attachment {0} to 'The Beatles - Revolver'" + .format(cover_path1), output) + self.assertIn("add cover attachment {0} to 'The Beatles - Abbey Road'" + .format(cover_path2), output) + + album1.load() + self.assertEqual(len(album1.attachments()), 1) + album2.load() + self.assertEqual(len(album2.attachments()), 1) class AttachmentDestinationTest(unittest.TestCase, AttachmentTestHelper): @@ -484,13 +501,10 @@ class AttachmentFactoryTest(unittest.TestCase, AttachmentTestHelper): self.assertEqual(len(attachments), 1) self.assertEqual(attachments[0].type, 'image') - # TODO Glob and RegExp. - # * Globs dont match files starting with a dot - # * Add extended bash globs. - # * Regexp must match full basename - def test_detect_config_types(self): + # TODO add extended bash globs + def test_detect_config_glob_types(self): self.config['attachments']['types'] = { - '.*\.jpg': 'image' + '*.jpg': 'image' } attachments = list(self.factory.detect('/path/to/cover.jpg')) @@ -500,15 +514,17 @@ class AttachmentFactoryTest(unittest.TestCase, AttachmentTestHelper): attachments = list(self.factory.detect('/path/to/cover.png')) self.assertEqual(len(attachments), 0) - def test_detect_multiple_types(self): - self.factory.register_detector(lambda _: 'a') - self.factory.register_detector(lambda _: 'b') + def test_detect_config_regexp_types(self): self.config['attachments']['types'] = { - '.*\.jpg$': 'c', - '.*/cover.jpg': 'd' + '/[abc]+.*\.(txt|md)/': 'xxx' } - attachments = list(self.factory.detect('/path/to/cover.jpg')) - self.assertItemsEqual(map(lambda a: a.type, attachments), 'abcd') + + attachments = list(self.factory.detect('/path/to/aabbcc.md')) + self.assertEqual(len(attachments), 1) + self.assertEqual(attachments[0].type, 'xxx') + + attachments = list(self.factory.detect('/path/to/aabbcc.mdx')) + self.assertEqual(len(attachments), 0) # factory.discover(album)