mirror of
https://github.com/beetbox/beets.git
synced 2026-02-20 06:14:22 +01:00
Attachments queries
This commit is contained in:
parent
54f11953fb
commit
315c43dcb8
4 changed files with 149 additions and 36 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ def capture_stdout():
|
|||
yield sys.stdout
|
||||
finally:
|
||||
sys.stdout = org
|
||||
print(org.getvalue())
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue