diff --git a/beets/attachments.py b/beets/attachments.py index 831aac9ea..dd8180538 100644 --- a/beets/attachments.py +++ b/beets/attachments.py @@ -20,7 +20,7 @@ import logging from argparse import ArgumentParser from beets import dbcore -from beets.dbcore.query import Query, AndQuery, MatchQuery +from beets.dbcore.query import Query, AndQuery, MatchQuery, OrQuery, FalseQuery from beets import util from beets.util import normpath, displayable_path from beets.util.functemplate import Template @@ -43,6 +43,7 @@ def config(key): def track_separators(): return config('track separators').get(list) + [os.sep] + def ref_type(entity): # FIXME prevents circular dependency from beets.library import Item, Album @@ -414,7 +415,7 @@ class AttachmentFactory(object): attachment = self.create(path, type, entity).add() return attachment - def find(self, attachment_query=None, entity_query=None): + def find(self, attachment_query=None, album_query=None, item_query=None): """Yield all attachments in the library matching `attachment_query` and their associated items matching `entity_query`. @@ -425,11 +426,44 @@ class AttachmentFactory(object): library.items(entity_query).attachments() """ # FIXME make this faster with joins - queries = [AttachmentEntityQuery(entity_query)] + queries = [] + from beets.library import Item, Album + if album_query: + queries.append(AttachmentEntityQuery(album_query, Album)) + if not item_query: + queries.append(AttachmentEntityQuery(FalseQuery(), Item)) + if item_query: + queries.append(AttachmentEntityQuery(item_query, Item)) + if not album_query: + queries.append(AttachmentEntityQuery(FalseQuery(), Album)) + + if queries: + queries = [OrQuery(queries)] if attachment_query: queries.append(attachment_query) return self._db._fetch(Attachment, AndQuery(queries)) + def parse_and_find(self, *query_strings): + from beets.library import get_query, Item, Album + queries = {Item: [], Album: [], Attachment: []} + for q in query_strings: + if q.startswith('a:'): + queries[Album].append(q[2:]) + elif q.startswith('t:'): + queries[Item].append(q[2:]) + elif q.startswith('e:'): + queries[Album].append(q[2:]) + queries[Item].append(q[2:]) + else: + queries[Attachment].append(q) + + for klass, qs in queries.items(): + if qs: + queries[klass] = get_query(qs, klass) + else: + queries[klass] = None + 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 entity prefix. @@ -438,7 +472,7 @@ class AttachmentFactory(object): is the item's path, excluding the extension. If the `local` argument is given the method returns a singleton - list consisting of the path `entity_preifix + separator + local` if + list consisting of the path `entity_prefix + separator + local` if it exists. Multiple separators are tried depening on the entity type. For albums the only separator is the directory separator. For items the separtors are configured by `attachments.item_sep` @@ -667,14 +701,15 @@ class AttachmentEntityQuery(Query): """Matches any attachment whose entity matches `entity_query`. """ - def __init__(self, entity_query): + def __init__(self, entity_query, entity_class=None): self.query = entity_query + self.entity_class = entity_class def match(self, attachment): - if self.query is not None: - return self.query.match(attachment.query) - else: - return True + entity = attachment.entity + if self.entity_class and not isinstance(entity, self.entity_class): + return False + return self.query.match(entity) class LibModelMixin(object): diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 8021e651c..f0ff4c40f 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -159,6 +159,25 @@ class AttachCommand(ui.Subcommand): default_commands.append(AttachCommand()) +class AttachListCommand(ui.Subcommand): + + def __init__(self): + super(AttachListCommand, self).__init__( + 'attachls', + help='list attachments by query' + 'track and move the attachment' + ) + + def func(self, lib, opts, args): + args = decargs(args) + factory = AttachmentFactory(lib) + for a in factory.parse_and_find(*args): + print('{0}: {1}'.format(a.type, displayable_path(a.path))) + + +default_commands.append(AttachListCommand()) + + # 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 87d0f16d3..8ae5245fe 100644 --- a/test/helper.py +++ b/test/helper.py @@ -87,6 +87,7 @@ def capture_stdout(): yield sys.stdout finally: sys.stdout = org + print(org.getvalue()) @contextmanager diff --git a/test/test_attachments.py b/test/test_attachments.py index f1724e5bb..c7f674cbb 100644 --- a/test/test_attachments.py +++ b/test/test_attachments.py @@ -14,8 +14,9 @@ import os +import itertools from _common import unittest -from helper import TestHelper, capture_log +from helper import TestHelper, capture_log, capture_stdout import beets.ui from beets.plugins import BeetsPlugin @@ -52,25 +53,28 @@ class AttachmentTestHelper(TestHelper): f.write(content) return path - def add_album(self, name='album name', touch=True): + def add_album(self, name='album name', touch=True, artist=None): """Add an album with one track to the library and create dummy track file. """ - album = Album(album=name) + album = Album(album=name, albumartist=artist) self.lib.add(album) item_path = os.path.join(self.lib.directory, name, 'track.mp3') - self.touch(item_path) + if touch: + self.touch(item_path) item = Item(album_id=album.id, path=item_path) self.lib.add(item) return album - def add_item(self, title): + def add_item(self, title='The Title', artist='The Artist', + album='The Album', touch=True): """Add item to the library and create dummy file. """ path = os.path.join(self.libdir, '{0}.mp3'.format(title)) - self.touch(path) - item = Item(title=title, path=path) + if touch: + self.touch(path) + item = Item(title=title, path=path, artist=artist, album=album) self.lib.add(item) return item @@ -88,6 +92,21 @@ class AttachmentTestHelper(TestHelper): self.lib.add(attachment) return attachment + def add_album_attachment(self, path='att.ext', type='atype', + album='The Album', artist='The Artist'): + album = self.add_album(name=album, artist=artist, touch=False) + if not os.path.isabs(path): + path = os.path.join(album.item_dir(), path) + return self.factory.add(path, type, album) + + def add_item_attachment(self, path='att.ext', type='atype', + album='Album', artist='Artist', title='Title'): + track = self.add_item(title=title, artist=artist, + album=album, touch=False) + if not os.path.isabs(path): + path = os.path.join(track.path, path) + return self.factory.add(path, type, track) + def add_attachment_plugin(self, ext, meta={}): def ext_detector(path): if path.endswith('.' + ext): @@ -97,10 +116,10 @@ class AttachmentTestHelper(TestHelper): if type == ext: return meta - log_plugin = BeetsPlugin() - log_plugin.attachment_detector = ext_detector - log_plugin.attachment_collector = collector - self.add_plugin(log_plugin) + plugin = BeetsPlugin() + plugin.attachment_detector = ext_detector + plugin.attachment_collector = collector + self.add_plugin(plugin) def set_path_template(self, *templates): self.config['attachments']['paths'] = templates @@ -111,6 +130,16 @@ class AttachmentTestHelper(TestHelper): def runcli(self, *args): beets.ui._raw_main(list(args), self.lib) + def cli_output(self, *args): + with capture_stdout() as output: + self.runcli(*args) + return output.getvalue().split('\n') + + def libpath(self, *components): + components = \ + itertools.chain(*map(lambda comp: comp.split('/'), components)) + return os.path.join(self.libdir, *components) + class AttachmentDestinationTest(unittest.TestCase, AttachmentTestHelper): """Test the `attachment.destination` property. @@ -342,7 +371,6 @@ class AttachmentFactoryTest(unittest.TestCase, AttachmentTestHelper): * factory.create() and meta data collectors * factory.detect() and type detectors (config and plugin) * factory.discover() - * factory.find() * factory.basename() """ @@ -435,21 +463,6 @@ class AttachmentFactoryTest(unittest.TestCase, AttachmentTestHelper): discovered = self.factory.discover(album, 'cover.jpg') self.assertEqual([attachment_path], discovered) - # factory.find() - # TODO extend - - def test_find_all_attachments(self): - item = self.add_item('track') - self.factory.add('/path', 'atype', item) - self.factory.add('/another_path', 'asecondtype', item) - - all_attachments = self.factory.find() - self.assertEqual(len(all_attachments), 2) - - attachment = all_attachments.get() - self.assertEqual(attachment.path, '/path') - self.assertEqual(attachment.type, 'atype') - # factory.basename() def test_item_basename(self): @@ -498,6 +511,51 @@ class AttachmentFactoryTest(unittest.TestCase, AttachmentTestHelper): self.assertEqual('y.cover.jpg', self.factory.basename(path, album)) +class AttachmentQueryTest(unittest.TestCase, AttachmentTestHelper): + + def setUp(self): + self.setup_beets() + + def tearDown(self): + self.teardown_beets() + + def test_all(self): + self.add_item_attachment(type='a') + self.add_album_attachment(type='b') + attachments = self.factory.find() + self.assertItemsEqual('ab', map(lambda a: a.type, attachments)) + + def test_type_query(self): + attachment = self.add_album_attachment(type='y') + self.add_album_attachment(type='another') + + res = self.factory.parse_and_find('type:y') + self.assertEqual(len(res), 1) + self.assertEqual(res.get().id, attachment.id) + + def test_album_name_query(self): + attachment = self.add_album_attachment(album='xxx') + self.add_item_attachment(album='xxx') + self.add_album_attachment(album='another') + res = self.factory.parse_and_find('a:album:xxx') + self.assertEqual(len(res), 1) + self.assertEqual(res.get().id, attachment.id) + + def test_album_search_query(self): + self.add_album_attachment(album='xxx') + self.add_album_attachment(artist='xxx') + self.add_album_attachment(album='another') + res = self.factory.parse_and_find('a:xxx') + self.assertEqual(len(res), 2) + + def test_entity_album_search(self): + self.add_album_attachment(album='xxx') + self.add_item_attachment(album='xxx') + self.add_album_attachment(album='another') + res = self.factory.parse_and_find('e:album:xxx') + self.assertEqual(len(res), 2) + + class EntityAttachmentsTest(unittest.TestCase, AttachmentTestHelper): """Test attachment queries on entities.