Attachments queries

This commit is contained in:
Thomas Scholtes 2014-08-14 22:22:04 +02:00
parent 54f11953fb
commit 315c43dcb8
4 changed files with 149 additions and 36 deletions

View file

@ -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):

View file

@ -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):

View file

@ -87,6 +87,7 @@ def capture_stdout():
yield sys.stdout
finally:
sys.stdout = org
print(org.getvalue())
@contextmanager

View file

@ -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.