diff --git a/beets/library.py b/beets/library.py index 333ff4238..05ef98f48 100644 --- a/beets/library.py +++ b/beets/library.py @@ -233,7 +233,7 @@ class LibModel(dbcore.Model): """Shared concrete functionality for Items and Albums. """ - _format_config_key = None + format_config_key = None """Config key that specifies how an instance should be formatted. """ @@ -256,7 +256,7 @@ class LibModel(dbcore.Model): def __format__(self, spec): if not spec: - spec = beets.config[self._format_config_key].get(unicode) + spec = beets.config[self.format_config_key].get(unicode) result = self.evaluate_template(spec) if isinstance(spec, bytes): # if spec is a byte string then we must return a one as well @@ -418,7 +418,7 @@ class Item(LibModel): _sorts = {'artist': SmartArtistSort} - _format_config_key = 'format_item' + format_config_key = 'format_item' @classmethod def _getters(cls): @@ -851,7 +851,7 @@ class Album(LibModel): """List of keys that are set on an album's items. """ - _format_config_key = 'format_album' + format_config_key = 'format_album' @classmethod def _getters(cls): diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 999067f53..b151f3602 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -592,6 +592,103 @@ def show_model_changes(new, old=None, fields=None, always=False): return bool(changes) +class CommonOptionsParser(optparse.OptionParser, object): + """Offers a simple way to add common formatting options. + + Options available include: + - matching albums instead of tracks: add_album_option() + - showing paths instead of items/albums: add_path_option() + - changing the format of displayed items/albums: add_format_option() + + The last one can have several behaviors: + - against a special target + - with a certain format + - autodetected target with the album option + + Each method is fully documented in the related method. + """ + def __init__(self, *args, **kwargs): + super(CommonOptionsParser, self).__init__(*args, **kwargs) + self._has_album = False + + def add_album_option(self, flags=('-a', '--album')): + """Add a -a/--album option to match albums instead of tracks. + + If used then the format option can auto-detect whether we're setting + the format for items or albums. + Sets the album property on the options extracted from the CLI. + """ + album = optparse.Option(*flags, action='store_true', + help='match albums instead of tracks') + self.add_option(album) + self._has_album = True + + def _set_format(self, option, opt_str, value, parser, target=None, + fmt=None): + """Internal callback that sets the correct format while parsing CLI + arguments. + """ + value = fmt or value and unicode(value) or '' + parser.values.format = value + if target: + config[target.format_config_key].set(value) + else: + if not self._has_album or not parser.values.album: + config[library.Item.format_config_key].set(value) + if not self._has_album or parser.values.album: + config[library.Album.format_config_key].set(value) + + def add_path_option(self, flags=('-p', '--path')): + """Add a -p/--path option to display the path instead of the default + format. + + By default this affects both items and albums. If add_album_option() + is used then the target will be autodetected. + + Sets the format property to u'$path' on the options extracted from the + CLI. + """ + path = optparse.Option(*flags, nargs=0, action='callback', + callback=self._set_format, + callback_kwargs={'fmt': '$path'}, + help='print paths for matched items or albums') + self.add_option(path) + + def add_format_option(self, flags=('-f', '--format'), target=None): + """Add -f/--format option to print some LibModel instances with a + custom format. + + `target` is optional and can be one of ``library.Item``, 'item', + ``library.Album`` and 'album'. + + Several behaviors are available: + - if `target` is given then the format is only applied to that + LibModel + - if the album option is used then the target will be autodetected + - otherwise the format is applied to both items and albums. + + Sets the format property on the options extracted from the CLI. + """ + kwargs = {} + if target: + if isinstance(target, basestring): + target = {'item': library.Item, + 'album': library.Album}[target] + kwargs['target'] = target + + opt = optparse.Option(*flags, action='callback', + callback=self._set_format, + callback_kwargs=kwargs, + help='print with custom format') + self.add_option(opt) + + def add_all_common_options(self): + """Add album, path and format options. + """ + self.add_album_option() + self.add_path_option() + self.add_format_option() + # Subcommand parsing infrastructure. # # This is a fairly generic subcommand parser for optparse. It is @@ -600,6 +697,7 @@ def show_model_changes(new, old=None, fields=None, always=False): # There you will also find a better description of the code and a more # succinct example program. + class Subcommand(object): """A subcommand of a root command-line application that may be invoked by a SubcommandOptionParser. @@ -609,10 +707,10 @@ class Subcommand(object): the subcommand; aliases are alternate names. parser is an OptionParser responsible for parsing the subcommand's options. help is a short description of the command. If no parser is - given, it defaults to a new, empty OptionParser. + given, it defaults to a new, empty CommonOptionsParser. """ self.name = name - self.parser = parser or optparse.OptionParser() + self.parser = parser or CommonOptionsParser() self.aliases = aliases self.help = help self.hide = hide @@ -635,7 +733,7 @@ class Subcommand(object): root_parser.get_prog_name().decode('utf8'), self.name) -class SubcommandsOptionParser(optparse.OptionParser, object): +class SubcommandsOptionParser(CommonOptionsParser): """A variant of OptionParser that parses subcommands and their arguments. """ @@ -924,6 +1022,8 @@ def _raw_main(args, lib=None): handling. """ parser = SubcommandsOptionParser() + parser.add_format_option(flags=('--format-item',), target=library.Item) + parser.add_format_option(flags=('--format-album',), target=library.Album) parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', diff --git a/beets/ui/commands.py b/beets/ui/commands.py index bea5dc91c..5d29093c9 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -957,7 +957,7 @@ default_commands.append(import_cmd) # list: Query and show library contents. -def list_items(lib, query, album, fmt): +def list_items(lib, query, album, fmt=''): """Print out items in lib matching query. If album, then search for albums instead of single items. """ @@ -970,23 +970,11 @@ def list_items(lib, query, album, fmt): def list_func(lib, opts, args): - fmt = '$path' if opts.path else opts.format - list_items(lib, decargs(args), opts.album, fmt) + list_items(lib, decargs(args), opts.album) list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',)) -list_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='show matching albums instead of tracks' -) -list_cmd.parser.add_option( - '-p', '--path', action='store_true', - help='print paths for matched items or albums' -) -list_cmd.parser.add_option( - '-f', '--format', action='store', - help='print with custom format', default='' -) +list_cmd.parser.add_all_common_options() list_cmd.func = list_func default_commands.append(list_cmd) @@ -1087,10 +1075,8 @@ def update_func(lib, opts, args): update_cmd = ui.Subcommand( 'update', help='update the library', aliases=('upd', 'up',) ) -update_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='match albums instead of tracks' -) +update_cmd.parser.add_album_option() +update_cmd.parser.add_format_option() update_cmd.parser.add_option( '-M', '--nomove', action='store_false', default=True, dest='move', help="don't move files in library" @@ -1099,10 +1085,6 @@ update_cmd.parser.add_option( '-p', '--pretend', action='store_true', help="show all changes but do nothing" ) -update_cmd.parser.add_option( - '-f', '--format', action='store', - help='print with custom format', default='' -) update_cmd.func = update_func default_commands.append(update_cmd) @@ -1151,10 +1133,7 @@ remove_cmd.parser.add_option( "-d", "--delete", action="store_true", help="also remove files from disk" ) -remove_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='match albums instead of tracks' -) +remove_cmd.parser.add_album_option() remove_cmd.func = remove_func default_commands.append(remove_cmd) @@ -1348,18 +1327,12 @@ modify_cmd.parser.add_option( '-W', '--nowrite', action='store_false', dest='write', help="don't write metadata (opposite of -w)" ) -modify_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='modify whole albums instead of tracks' -) +modify_cmd.parser.add_album_option() +modify_cmd.parser.add_format_option(target='item') modify_cmd.parser.add_option( '-y', '--yes', action='store_true', help='skip confirmation' ) -modify_cmd.parser.add_option( - '-f', '--format', action='store', - help='print with custom format', default='' -) modify_cmd.func = modify_func default_commands.append(modify_cmd) @@ -1405,10 +1378,7 @@ move_cmd.parser.add_option( '-c', '--copy', default=False, action='store_true', help='copy instead of moving' ) -move_cmd.parser.add_option( - '-a', '--album', default=False, action='store_true', - help='match whole albums instead of tracks' -) +move_cmd.parser.add_album_option() move_cmd.func = move_func default_commands.append(move_cmd) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 352c3c94d..d4bc6d32b 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -359,7 +359,7 @@ class ConvertPlugin(BeetsPlugin): self.config['pretend'].get(bool) if not pretend: - ui.commands.list_items(lib, ui.decargs(args), opts.album, '') + ui.commands.list_items(lib, ui.decargs(args), opts.album) if not (opts.yes or ui.input_yn("Convert? (Y/n)")): return