diff --git a/beetsplug/info.py b/beetsplug/info.py index 9dd78f250..7a5a47b84 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -19,6 +19,7 @@ from __future__ import (division, absolute_import, print_function, unicode_literals) import os +import re from beets.plugins import BeetsPlugin from beets import ui @@ -77,7 +78,7 @@ def update_summary(summary, tags): def print_data(data): - path = data.pop('path') + path = data.pop('path', None) formatted = {} for key, value in data.iteritems(): if isinstance(value, list): @@ -85,6 +86,9 @@ def print_data(data): if value is not None: formatted[key] = value + if len(formatted) == 0: + return + maxwidth = max(len(key) for key in formatted) lineformat = u'{{0:>{0}}}: {{1}}'.format(maxwidth) @@ -107,6 +111,9 @@ class InfoPlugin(BeetsPlugin): help='show library fields instead of tags') cmd.parser.add_option('-s', '--summarize', action='store_true', help='summarize the tags of all files') + cmd.parser.add_option('-i', '--include-keys', default=[], + action='append', dest='included_keys', + help='comma separated list of keys to show') return [cmd] def run(self, lib, opts, args): @@ -128,6 +135,11 @@ class InfoPlugin(BeetsPlugin): else: data_collector = tag_data + included_keys = [] + for keys in opts.included_keys: + included_keys.extend(keys.split(',')) + key_filter = make_key_filter(included_keys) + first = True summary = {} for data_emitter in data_collector(lib, ui.decargs(args)): @@ -137,6 +149,9 @@ class InfoPlugin(BeetsPlugin): self._log.error(u'cannot read file: {0}', ex) continue + path = data.get('path') + data = key_filter(data) + data['path'] = path # always show path if opts.summarize: update_summary(summary, data) else: @@ -147,3 +162,33 @@ class InfoPlugin(BeetsPlugin): if opts.summarize: print_data(summary) + + +def make_key_filter(include): + """Return a function that filters a dictionary. + + The returned filter takes a dictionary and returns another + dictionary that only includes the key-value pairs where the key + glob-matches one of the keys in `include`. + """ + if not include: + return identity + + matchers = [] + for key in include: + key = re.escape(key) + key = key.replace(r'\*', '.*') + matchers.append(re.compile(key + '$')) + + def filter(data): + filtered = dict() + for key, value in data.items(): + if any(map(lambda m: m.match(key), matchers)): + filtered[key] = value + return filtered + + return filter + + +def identity(val): + return val diff --git a/docs/changelog.rst b/docs/changelog.rst index b8fc19dac..2f540814e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,8 @@ Features: ``art_filename`` configuration option. :bug:`1258` * :doc:`/plugins/fetchart`: There's a new Wikipedia image source that uses DBpedia to find albums. Thanks to Tom Jaspers. :bug:`1194` +* :doc:`/plugins/info`: New options ``-i`` to display only given + properties. :bug:`1287` Core changes: diff --git a/docs/plugins/info.rst b/docs/plugins/info.rst index 944f17c55..5bfde0b41 100644 --- a/docs/plugins/info.rst +++ b/docs/plugins/info.rst @@ -18,7 +18,18 @@ your library:: $ beet info beatles -Command-line options include: +If you just want to see specific properties you can use the +``--include-keys`` option to filter them. The argument is a +comma-separated list of simple glob patterns where ``*`` matches any +string. For example:: + + $ beet info -i 'title,mb*' beatles + +Will only show the ``title`` property and all properties starting with +``mb``. You can add the ``-i`` option multiple times to the command +line. + +Additional command-line options include: * ``--library`` or ``-l``: Show data from the library database instead of the files' tags. diff --git a/test/helper.py b/test/helper.py index 81a0b00cc..64db9e8b1 100644 --- a/test/helper.py +++ b/test/helper.py @@ -407,7 +407,7 @@ class TestHelper(object): def run_with_output(self, *args): with capture_stdout() as out: self.run_command(*args) - return out.getvalue() + return out.getvalue().decode('utf-8') # Safe file operations diff --git a/test/test_info.py b/test/test_info.py index 09668ceb2..d528517ed 100644 --- a/test/test_info.py +++ b/test/test_info.py @@ -19,6 +19,7 @@ from test._common import unittest from test.helper import TestHelper from beets.mediafile import MediaFile +from beets.util import displayable_path class InfoTest(unittest.TestCase, TestHelper): @@ -52,17 +53,17 @@ class InfoTest(unittest.TestCase, TestHelper): self.assertNotIn('composer:', out) def test_item_query(self): - items = self.add_item_fixtures(count=2) - items[0].album = 'xxxx' - items[0].write() - items[0].album = 'yyyy' - items[0].store() + item1, item2 = self.add_item_fixtures(count=2) + item1.album = 'xxxx' + item1.write() + item1.album = 'yyyy' + item1.store() out = self.run_with_output('album:yyyy') - self.assertIn(items[0].path, out) - self.assertIn(b'album: xxxx', out) + self.assertIn(displayable_path(item1.path), out) + self.assertIn(u'album: xxxx', out) - self.assertNotIn(items[1].path, out) + self.assertNotIn(displayable_path(item2.path), out) def test_item_library_query(self): item, = self.add_item_fixtures() @@ -70,8 +71,8 @@ class InfoTest(unittest.TestCase, TestHelper): item.store() out = self.run_with_output('--library', 'album:xxxx') - self.assertIn(item.path, out) - self.assertIn(b'album: xxxx', out) + self.assertIn(displayable_path(item.path), out) + self.assertIn(u'album: xxxx', out) def test_collect_item_and_path(self): path = self.create_mediafile_fixture() @@ -88,9 +89,18 @@ class InfoTest(unittest.TestCase, TestHelper): mediafile.save() out = self.run_with_output('--summarize', 'album:AAA', path) - self.assertIn('album: AAA', out) - self.assertIn('tracktotal: 5', out) - self.assertIn('title: [various]', out) + self.assertIn(u'album: AAA', out) + self.assertIn(u'tracktotal: 5', out) + self.assertIn(u'title: [various]', out) + + def test_include_pattern(self): + item = self.add_item(album='xxxx') + + out = self.run_with_output('--library', 'album:xxxx', + '--include-keys', '*lbu*') + self.assertIn(displayable_path(item.path), out) + self.assertNotIn(u'title:', out) + self.assertIn(u'album: xxxx', out) def suite():