Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set field values on the import command line #1881 #2581

Merged
merged 10 commits into from
Jun 13, 2017
3 changes: 2 additions & 1 deletion beets/config_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import:
pretend: false
search_ids: []
duplicate_action: ask
bell: no
bell: no
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed a redundant space after 'bell: no'

set_fields: {}

clutter: ["Thumbs.DB", ".DS_Store"]
ignore: [".*", "*~", "System Volume Information", "lost+found"]
Expand Down
39 changes: 38 additions & 1 deletion beets/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ class ImportTask(BaseImportTask):
from the `candidates` list.

* `find_duplicates()` Returns a list of albums from `lib` with the
same artist and album name as the task.
same artist and album name as the task.

* `apply_metadata()` Sets the attributes of the items from the
task's `match` attribute.
Expand All @@ -429,6 +429,9 @@ class ImportTask(BaseImportTask):
* `manipulate_files()` Copy, move, and write files depending on the
session configuration.

* `set_fields()` Sets the fields given at CLI or configuration to
the specified values.

* `finalize()` Update the import progress and cleanup the file
system.
"""
Expand Down Expand Up @@ -530,6 +533,19 @@ def remove_duplicates(self, lib):
util.prune_dirs(os.path.dirname(item.path),
lib.directory)

def set_fields(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A docstring here might be nice to have! (Perhaps even nicer than the summary in the overall "step sequence" in the overall class docstring.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

"""Sets the fields given at CLI or configuration to the specified
values.
"""
for field, view in config['import']['set_fields'].items():
value = view.get()
log.debug(u'Set field {1}={2} for {0}',
displayable_path(self.paths),
field,
value)
self.album[field] = value
self.album.store()

def finalize(self, session):
"""Save progress, clean up files, and emit plugin event.
"""
Expand Down Expand Up @@ -877,6 +893,19 @@ def choose_match(self, session):
def reload(self):
self.item.load()

def set_fields(self):
"""Sets the fields given at CLI or configuration to the specified
values.
"""
for field, view in config['import']['set_fields'].items():
value = view.get()
log.debug(u'Set field {1}={2} for {0}',
displayable_path(self.paths),
field,
value)
self.item[field] = value
self.item.store()


# FIXME The inheritance relationships are inverted. This is why there
# are so many methods which pass. More responsibility should be delegated to
Expand Down Expand Up @@ -1385,6 +1414,14 @@ def apply_choice(session, task):

task.add(session.lib)

# If ``set_fields`` is set, set those fields to the
# configured values.
# NOTE: This cannot be done before the ``task.add()`` call above,
# because then the ``ImportTask`` won't have an `album` for which
# it can set the fields.
if config['import']['set_fields']:
task.set_fields()


@pipeline.mutator_stage
def plugin_stage(session, func, task):
Expand Down
28 changes: 28 additions & 0 deletions beets/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,34 @@ def show_path_changes(path_changes):
pad = max_width - len(source)
log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest)

# Helper functions for option parsing.


def _store_dict(option, opt_str, value, parser):
"""Custom action callback to parse options which have ``key=value``
pairs as values. All such pairs passed for this option are
aggregated into a dictionary.
"""
dest = option.dest
option_values = getattr(parser.values, dest, None)

if option_values is None:
# This is the first supplied ``key=value`` pair of option.
# Initialize empty dictionary and get a reference to it.
setattr(parser.values, dest, dict())
option_values = getattr(parser.values, dest)

try:
key, value = map(lambda s: util.text_string(s), value.split('='))
if not (key and value):
raise ValueError
except ValueError:
raise UserError(
"supplied argument `{0}' is not of the form `key=value'"
.format(value))

option_values[key] = value


class CommonOptionsParser(optparse.OptionParser, object):
"""Offers a simple way to add common formatting options.
Expand Down
7 changes: 7 additions & 0 deletions beets/ui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from beets import logging
from beets.util.confit import _package_path
import six
from . import _store_dict

VARIOUS_ARTISTS = u'Various Artists'
PromptChoice = namedtuple('PromptChoice', ['short', 'long', 'callback'])
Expand Down Expand Up @@ -1017,6 +1018,12 @@ def import_func(lib, opts, args):
metavar='ID',
help=u'restrict matching to a specific metadata backend ID'
)
import_cmd.parser.add_option(
u'--set', dest='set_fields', action='callback',
callback=_store_dict,
metavar='FIELD=VALUE',
help=u'set the given fields to the supplied values'
)
import_cmd.func = import_func
default_commands.append(import_cmd)

Expand Down
8 changes: 8 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ Here's a full list of new features:
Thanks to :user:`jansol`.
:bug:`2488`
:bug:`2524`
* A new field, ``composer_sort``, is now supported and fetched from
MusicBrainz.
Thanks to :user:`dosoe`.
:bug:`2519` :bug:`2529`
* It is now possible to set fields to certain values during import, using
either the `importer.set_fields` dictionary in the config file, or by
passing one or more `--set field=value` options on the command-line.
:bug: `1881`

There are also quite a few fixes:

Expand Down
10 changes: 10 additions & 0 deletions docs/reference/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ Optional command flags:
searching for other candidates by using the ``--search-id SEARCH_ID`` option.
Multiple IDs can be specified by simply repeating the option several times.

* You can supply ``--set`` options with ``field=value`` pairs to assign to
those fields the specified values on import, in addition to such field/value
pairs defined in the ``importer.set_fields`` dictionary in the configuration
file. Make sure to use an option per field/value pair, like so::

beet import --set genre="Alternative Rock" --set mood="emotional"

Note that values for the fields specified on the command-line override the
ones defined for those fields in the configuration file.

.. _rarfile: https://pypi.python.org/pypi/rarfile/2.2

.. only:: html
Expand Down
20 changes: 20 additions & 0 deletions docs/reference/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,26 @@ Ring the terminal bell to get your attention when the importer needs your input.

Default: ``no``.

.. _set_fields:

set_fields
~~~~~~~~~~

A dictionary of field/value pairs, each one used to set a field to the
corresponding value during import.

Example: ::

set_fields:
genre: 'To Listen'
collection: 'Unordered'

Note that field/value pairs supplied via ``--set`` options on the
command-line are processed in addition to those specified here. Those values
override the ones defined here in the case of fields with the same name.

Default: ``{}`` (empty).

.. _musicbrainz-config:

MusicBrainz Options
Expand Down
64 changes: 64 additions & 0 deletions test/test_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,38 @@ def test_import_single_files(self):
self.assertEqual(len(self.lib.items()), 2)
self.assertEqual(len(self.lib.albums()), 2)

def test_set_fields(self):
genre = u"\U0001F3B7 Jazz"
collection = u"To Listen"

config['import']['set_fields'] = {
u'collection': collection,
u'genre': genre
}

# As-is item import.
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()

for item in self.lib.items():
item.load() # TODO: Not sure this is necessary.
self.assertEqual(item.genre, genre)
self.assertEqual(item.collection, collection)
# Remove item from library to test again with APPLY choice.
item.remove()

# Autotagged.
self.assertEqual(self.lib.albums().get(), None)
self.importer.clear_choices()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()

for item in self.lib.items():
item.load()
self.assertEqual(item.genre, genre)
self.assertEqual(item.collection, collection)


class ImportTest(_common.TestCase, ImportHelper):
"""Test APPLY, ASIS and SKIP choices.
Expand Down Expand Up @@ -672,6 +704,38 @@ def test_asis_no_data_source(self):
with self.assertRaises(AttributeError):
self.lib.items().get().data_source

def test_set_fields(self):
genre = u"\U0001F3B7 Jazz"
collection = u"To Listen"

config['import']['set_fields'] = {
u'collection': collection,
u'genre': genre
}

# As-is album import.
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()

for album in self.lib.albums():
album.load() # TODO: Not sure this is necessary.
self.assertEqual(album.genre, genre)
self.assertEqual(album.collection, collection)
# Remove album from library to test again with APPLY choice.
album.remove()

# Autotagged.
self.assertEqual(self.lib.albums().get(), None)
self.importer.clear_choices()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()

for album in self.lib.albums():
album.load()
self.assertEqual(album.genre, genre)
self.assertEqual(album.collection, collection)


class ImportTracksTest(_common.TestCase, ImportHelper):
"""Test TRACKS and APPLY choice.
Expand Down