Skip to content

Commit e0ce507

Browse files
committed
Merge pull request #2581 from bartkl/master
Set field values on the import command line #1881
2 parents f65653a + 2eb4e3d commit e0ce507

File tree

8 files changed

+177
-2
lines changed

8 files changed

+177
-2
lines changed

beets/config_default.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import:
2525
pretend: false
2626
search_ids: []
2727
duplicate_action: ask
28-
bell: no
28+
bell: no
29+
set_fields: {}
2930

3031
clutter: ["Thumbs.DB", ".DS_Store"]
3132
ignore: [".*", "*~", "System Volume Information", "lost+found"]

beets/importer.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ class ImportTask(BaseImportTask):
419419
from the `candidates` list.
420420
421421
* `find_duplicates()` Returns a list of albums from `lib` with the
422-
same artist and album name as the task.
422+
same artist and album name as the task.
423423
424424
* `apply_metadata()` Sets the attributes of the items from the
425425
task's `match` attribute.
@@ -429,6 +429,9 @@ class ImportTask(BaseImportTask):
429429
* `manipulate_files()` Copy, move, and write files depending on the
430430
session configuration.
431431
432+
* `set_fields()` Sets the fields given at CLI or configuration to
433+
the specified values.
434+
432435
* `finalize()` Update the import progress and cleanup the file
433436
system.
434437
"""
@@ -530,6 +533,19 @@ def remove_duplicates(self, lib):
530533
util.prune_dirs(os.path.dirname(item.path),
531534
lib.directory)
532535

536+
def set_fields(self):
537+
"""Sets the fields given at CLI or configuration to the specified
538+
values.
539+
"""
540+
for field, view in config['import']['set_fields'].items():
541+
value = view.get()
542+
log.debug(u'Set field {1}={2} for {0}',
543+
displayable_path(self.paths),
544+
field,
545+
value)
546+
self.album[field] = value
547+
self.album.store()
548+
533549
def finalize(self, session):
534550
"""Save progress, clean up files, and emit plugin event.
535551
"""
@@ -877,6 +893,19 @@ def choose_match(self, session):
877893
def reload(self):
878894
self.item.load()
879895

896+
def set_fields(self):
897+
"""Sets the fields given at CLI or configuration to the specified
898+
values.
899+
"""
900+
for field, view in config['import']['set_fields'].items():
901+
value = view.get()
902+
log.debug(u'Set field {1}={2} for {0}',
903+
displayable_path(self.paths),
904+
field,
905+
value)
906+
self.item[field] = value
907+
self.item.store()
908+
880909

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

13861415
task.add(session.lib)
13871416

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

13891426
@pipeline.mutator_stage
13901427
def plugin_stage(session, func, task):

beets/ui/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,34 @@ def show_path_changes(path_changes):
769769
pad = max_width - len(source)
770770
log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest)
771771

772+
# Helper functions for option parsing.
773+
774+
775+
def _store_dict(option, opt_str, value, parser):
776+
"""Custom action callback to parse options which have ``key=value``
777+
pairs as values. All such pairs passed for this option are
778+
aggregated into a dictionary.
779+
"""
780+
dest = option.dest
781+
option_values = getattr(parser.values, dest, None)
782+
783+
if option_values is None:
784+
# This is the first supplied ``key=value`` pair of option.
785+
# Initialize empty dictionary and get a reference to it.
786+
setattr(parser.values, dest, dict())
787+
option_values = getattr(parser.values, dest)
788+
789+
try:
790+
key, value = map(lambda s: util.text_string(s), value.split('='))
791+
if not (key and value):
792+
raise ValueError
793+
except ValueError:
794+
raise UserError(
795+
"supplied argument `{0}' is not of the form `key=value'"
796+
.format(value))
797+
798+
option_values[key] = value
799+
772800

773801
class CommonOptionsParser(optparse.OptionParser, object):
774802
"""Offers a simple way to add common formatting options.

beets/ui/commands.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from beets import logging
4141
from beets.util.confit import _package_path
4242
import six
43+
from . import _store_dict
4344

4445
VARIOUS_ARTISTS = u'Various Artists'
4546
PromptChoice = namedtuple('PromptChoice', ['short', 'long', 'callback'])
@@ -1017,6 +1018,12 @@ def import_func(lib, opts, args):
10171018
metavar='ID',
10181019
help=u'restrict matching to a specific metadata backend ID'
10191020
)
1021+
import_cmd.parser.add_option(
1022+
u'--set', dest='set_fields', action='callback',
1023+
callback=_store_dict,
1024+
metavar='FIELD=VALUE',
1025+
help=u'set the given fields to the supplied values'
1026+
)
10201027
import_cmd.func = import_func
10211028
default_commands.append(import_cmd)
10221029

docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ Here's a full list of new features:
8888
Thanks to :user:`jansol`.
8989
:bug:`2488`
9090
:bug:`2524`
91+
* A new field, ``composer_sort``, is now supported and fetched from
92+
MusicBrainz.
93+
Thanks to :user:`dosoe`.
94+
:bug:`2519` :bug:`2529`
95+
* It is now possible to set fields to certain values during import, using
96+
either the `importer.set_fields` dictionary in the config file, or by
97+
passing one or more `--set field=value` options on the command-line.
98+
:bug: `1881`
9199

92100
There are also quite a few fixes:
93101

docs/reference/cli.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ Optional command flags:
138138
searching for other candidates by using the ``--search-id SEARCH_ID`` option.
139139
Multiple IDs can be specified by simply repeating the option several times.
140140

141+
* You can supply ``--set`` options with ``field=value`` pairs to assign to
142+
those fields the specified values on import, in addition to such field/value
143+
pairs defined in the ``importer.set_fields`` dictionary in the configuration
144+
file. Make sure to use an option per field/value pair, like so::
145+
146+
beet import --set genre="Alternative Rock" --set mood="emotional"
147+
148+
Note that values for the fields specified on the command-line override the
149+
ones defined for those fields in the configuration file.
150+
141151
.. _rarfile: https://pypi.python.org/pypi/rarfile/2.2
142152

143153
.. only:: html

docs/reference/config.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,26 @@ Ring the terminal bell to get your attention when the importer needs your input.
586586

587587
Default: ``no``.
588588

589+
.. _set_fields:
590+
591+
set_fields
592+
~~~~~~~~~~
593+
594+
A dictionary of field/value pairs, each one used to set a field to the
595+
corresponding value during import.
596+
597+
Example: ::
598+
599+
set_fields:
600+
genre: 'To Listen'
601+
collection: 'Unordered'
602+
603+
Note that field/value pairs supplied via ``--set`` options on the
604+
command-line are processed in addition to those specified here. Those values
605+
override the ones defined here in the case of fields with the same name.
606+
607+
Default: ``{}`` (empty).
608+
589609
.. _musicbrainz-config:
590610

591611
MusicBrainz Options

test/test_importer.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,38 @@ def test_import_single_files(self):
543543
self.assertEqual(len(self.lib.items()), 2)
544544
self.assertEqual(len(self.lib.albums()), 2)
545545

546+
def test_set_fields(self):
547+
genre = u"\U0001F3B7 Jazz"
548+
collection = u"To Listen"
549+
550+
config['import']['set_fields'] = {
551+
u'collection': collection,
552+
u'genre': genre
553+
}
554+
555+
# As-is item import.
556+
self.assertEqual(self.lib.albums().get(), None)
557+
self.importer.add_choice(importer.action.ASIS)
558+
self.importer.run()
559+
560+
for item in self.lib.items():
561+
item.load() # TODO: Not sure this is necessary.
562+
self.assertEqual(item.genre, genre)
563+
self.assertEqual(item.collection, collection)
564+
# Remove item from library to test again with APPLY choice.
565+
item.remove()
566+
567+
# Autotagged.
568+
self.assertEqual(self.lib.albums().get(), None)
569+
self.importer.clear_choices()
570+
self.importer.add_choice(importer.action.APPLY)
571+
self.importer.run()
572+
573+
for item in self.lib.items():
574+
item.load()
575+
self.assertEqual(item.genre, genre)
576+
self.assertEqual(item.collection, collection)
577+
546578

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

707+
def test_set_fields(self):
708+
genre = u"\U0001F3B7 Jazz"
709+
collection = u"To Listen"
710+
711+
config['import']['set_fields'] = {
712+
u'collection': collection,
713+
u'genre': genre
714+
}
715+
716+
# As-is album import.
717+
self.assertEqual(self.lib.albums().get(), None)
718+
self.importer.add_choice(importer.action.ASIS)
719+
self.importer.run()
720+
721+
for album in self.lib.albums():
722+
album.load() # TODO: Not sure this is necessary.
723+
self.assertEqual(album.genre, genre)
724+
self.assertEqual(album.collection, collection)
725+
# Remove album from library to test again with APPLY choice.
726+
album.remove()
727+
728+
# Autotagged.
729+
self.assertEqual(self.lib.albums().get(), None)
730+
self.importer.clear_choices()
731+
self.importer.add_choice(importer.action.APPLY)
732+
self.importer.run()
733+
734+
for album in self.lib.albums():
735+
album.load()
736+
self.assertEqual(album.genre, genre)
737+
self.assertEqual(album.collection, collection)
738+
675739

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

0 commit comments

Comments
 (0)