Info plugin can filter properties in output

Resolves #1287
This commit is contained in:
Thomas Scholtes 2015-02-01 17:25:06 +01:00
parent 43e9044843
commit 482008bf1d
5 changed files with 84 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

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