diff --git a/beets/attachments.py b/beets/attachments.py index a49bcbf8c..15ed9fe2d 100644 --- a/beets/attachments.py +++ b/beets/attachments.py @@ -15,8 +15,10 @@ import re import urlparse +import optparse from argparse import ArgumentParser +from beets.plugins import find_plugins from beets import dbcore from beets.dbcore.query import Query, AndQuery @@ -232,9 +234,12 @@ class AttachmentFactory(object): """ self._collectors.append(collector) - def register_plugin(self, plugin): - self.register_discoverer(plugin.attachment_discoverer) - self.register_collector(plugin.attachment_collector) + def register_plugins(self, plugins): + for plugin in plugins: + if hasattr(plugin, 'attachment_discoverer'): + self.register_discoverer(plugin.attachment_discoverer) + if hasattr(plugin, 'attachment_collector'): + self.register_collector(plugin.attachment_collector) def _discover_types(self, path): types = [] @@ -252,6 +257,8 @@ class AttachmentFactory(object): all_meta = {} for collector in self._collectors: try: + # TODO maybe we should provide file handle for checking + # content meta = collector(type, path) if isinstance(meta, dict): all_meta.update(meta) @@ -300,7 +307,54 @@ class AttachmentCommand(ArgumentParser): pass +class AttachCommand(object): + """Duck type for ui.Subcommand + """ + + def __init__(self): + self.name = 'attach' + self.parser = optparse.OptionParser() + self.aliases = () + self.help = 'create an attachment for an album or a ' \ + 'track and move the attachment' + self.hide = False + + self.parser.add_option( + '-c', '--copy', action='store_true', dest='copy', + help='copy attachment intead of moving them' + ) + self.parser.add_option( + '--track', action='store_true', dest='track', + help='attach path to the tracks matched by the query' + ) + self.parser.add_option( + '-t', '--type', dest='type', + help='create one attachment with this type', + ) + + def func(self, lib, opts, args): + # FIXME prevents circular dependency + from beets.ui import decargs + factory = AttachmentFactory(lib) + factory.register_plugins(find_plugins()) + path = args.pop(0) + + if opts.track: + entities = lib.items(decargs(args)) + else: + entities = lib.albums(decargs(args)) + + for entity in entities: + if opts.type: + factory.create(path, opts.type, entity).add() + else: + for attachment in factory.discover(path, entity): + attachment.add() + + class AttachmentRefQuery(Query): + """Matches any attachment whose entity is `entity`. + """ def __init__(self, entity): self.entity = entity @@ -314,6 +368,8 @@ class AttachmentRefQuery(Query): class AttachmentEntityQuery(Query): + """Matches any attachment whose entity matches `entity_query`. + """ def __init__(self, entity_query): self.query = entity_query diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 14f2bf0a4..6474114d1 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -38,6 +38,7 @@ from beets.util import syspath, normpath, ancestry, displayable_path from beets.util.functemplate import Template from beets import library from beets import config +from beets import attachments from beets.util.confit import _package_path VARIOUS_ARTISTS = u'Various Artists' @@ -78,6 +79,9 @@ def _do_query(lib, query, album, also_items=True): return items, albums +default_commands.append(attachments.AttachCommand()) + + # 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 4bcab88ad..2038aea15 100644 --- a/test/helper.py +++ b/test/helper.py @@ -170,6 +170,14 @@ class TestHelper(object): beets.plugins.load_plugins(plugins) beets.plugins.find_plugins() + def add_plugin(self, plugin): + """Add a plugin instance to the list returned by + `plugins.find_plugins()`. + """ + def create_plugin(): + return plugin + beets.plugins._classes.add(create_plugin) + def unload_plugins(self): """Unload all plugins and remove the from the configuration. """ diff --git a/test/test_attachments.py b/test/test_attachments.py index 62069228d..5ff0313cd 100644 --- a/test/test_attachments.py +++ b/test/test_attachments.py @@ -13,8 +13,13 @@ # included in all copies or substantial portions of the Software. +import os +from tempfile import mkstemp from _common import unittest +from helper import TestHelper +import beets.ui +from beets.plugins import BeetsPlugin from beets.attachments import AttachmentFactory from beets.library import Library, Album, Item @@ -87,6 +92,78 @@ class EntityAttachmentsTest(unittest.TestCase): [attachment.id]) +class AttachCommandTest(unittest.TestCase, TestHelper): + + def setUp(self): + self.setup_beets() + self.setup_log_attachment_plugin() + self.tmp_files = [] + + def tearDown(self): + self.teardown_beets() + self.unload_plugins() + for p in self.tmp_files: + os.remove(p) + + def test_attach_to_album(self): + album = Album(album='albumtitle') + self.lib.add(album) + + attachment_path = self.mkstemp('.log') + self.runcli('attach', attachment_path, 'albumtitle') + attachment = album.attachments().get() + self.assertEqual(attachment.type, 'log') + + def test_attach_to_album_and_move(self): + self.skipTest('Not implemented') + + def test_file_relative_to_album_dir(self): + self.skipTest('Not implemented') + + def test_attach_to_item(self): + item = Item(title='tracktitle') + self.lib.add(item) + + attachment_path = self.mkstemp('.log') + self.runcli('attach', '--track', attachment_path, 'tracktitle') + attachment = item.attachments().get() + self.assertEqual(attachment.type, 'log') + + def test_attach_to_item_and_move(self): + self.skipTest('Not implemented') + + def test_user_type(self): + album = Album(album='albumtitle') + self.lib.add(album) + + attachment_path = self.mkstemp() + self.runcli('attach', '-t', 'customtype', attachment_path, 'albumtitle') + attachment = album.attachments().get() + self.assertEqual(attachment.type, 'customtype') + + def test_unknown_warning(self): + self.skipTest('Not implemented') + + # Helpers + + def runcli(self, *args): + beets.ui._raw_main(list(args), self.lib) + + def mkstemp(self, suffix=''): + (handle, path) = mkstemp(suffix) + os.close(handle) + self.tmp_files.append(path) + return path + + def setup_log_attachment_plugin(self): + def log_discoverer(path): + if path.endswith('.log'): + return 'log' + log_plugin = BeetsPlugin() + log_plugin.attachment_discoverer = log_discoverer + self.add_plugin(log_plugin) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)