mirror of
https://github.com/beetbox/beets.git
synced 2026-02-24 08:12:54 +01:00
Merge pull request #1346 from brunal/subcommand-auto-format-path
Generalize --path, --format and --album options
This commit is contained in:
commit
f6c6c35614
21 changed files with 341 additions and 120 deletions
|
|
@ -61,8 +61,8 @@ ui:
|
|||
action_default: turquoise
|
||||
action: blue
|
||||
|
||||
list_format_item: $artist - $album - $title
|
||||
list_format_album: $albumartist - $album
|
||||
format_item: $artist - $album - $title
|
||||
format_album: $albumartist - $album
|
||||
time_format: '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
sort_album: albumartist+ album+
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ class Item(LibModel):
|
|||
|
||||
_sorts = {'artist': SmartArtistSort}
|
||||
|
||||
_format_config_key = 'list_format_item'
|
||||
_format_config_key = 'format_item'
|
||||
|
||||
@classmethod
|
||||
def _getters(cls):
|
||||
|
|
@ -877,7 +877,7 @@ class Album(LibModel):
|
|||
"""List of keys that are set on an album's items.
|
||||
"""
|
||||
|
||||
_format_config_key = 'list_format_album'
|
||||
_format_config_key = 'format_album'
|
||||
|
||||
@classmethod
|
||||
def _getters(cls):
|
||||
|
|
|
|||
|
|
@ -592,6 +592,119 @@ 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._album_flags = False
|
||||
# this serves both as an indicator that we offer the feature AND allows
|
||||
# us to check whether it has been specified on the CLI - bypassing the
|
||||
# fact that arguments may be in any order
|
||||
|
||||
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._album_flags = set(flags)
|
||||
|
||||
def _set_format(self, option, opt_str, value, parser, target=None,
|
||||
fmt=None, store_true=False):
|
||||
"""Internal callback that sets the correct format while parsing CLI
|
||||
arguments.
|
||||
"""
|
||||
if store_true:
|
||||
setattr(parser.values, option.dest, True)
|
||||
|
||||
value = fmt or value and unicode(value) or ''
|
||||
parser.values.format = value
|
||||
if target:
|
||||
config[target._format_config_key].set(value)
|
||||
else:
|
||||
if self._album_flags:
|
||||
if parser.values.album:
|
||||
target = library.Album
|
||||
else:
|
||||
# the option is either missing either not parsed yet
|
||||
if self._album_flags & set(parser.rargs):
|
||||
target = library.Album
|
||||
else:
|
||||
target = library.Item
|
||||
config[target._format_config_key].set(value)
|
||||
else:
|
||||
config[library.Item._format_config_key].set(value)
|
||||
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',
|
||||
'store_true': True},
|
||||
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 +713,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 +723,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 +749,7 @@ class Subcommand(object):
|
|||
root_parser.get_prog_name().decode('utf8'), self.name)
|
||||
|
||||
|
||||
class SubcommandsOptionParser(optparse.OptionParser):
|
||||
class SubcommandsOptionParser(CommonOptionsParser):
|
||||
"""A variant of OptionParser that parses subcommands and their
|
||||
arguments.
|
||||
"""
|
||||
|
|
@ -653,7 +767,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
kwargs['add_help_option'] = False
|
||||
|
||||
# Super constructor.
|
||||
optparse.OptionParser.__init__(self, *args, **kwargs)
|
||||
super(SubcommandsOptionParser, self).__init__(*args, **kwargs)
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
self.disable_interspersed_args()
|
||||
|
|
@ -670,7 +784,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
# Add the list of subcommands to the help message.
|
||||
def format_help(self, formatter=None):
|
||||
# Get the original help message, to which we will append.
|
||||
out = optparse.OptionParser.format_help(self, formatter)
|
||||
out = super(SubcommandsOptionParser, self).format_help(formatter)
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
|
||||
|
|
@ -871,6 +985,17 @@ def _configure(options):
|
|||
u'See documentation for more info.')
|
||||
config['ui']['color'].set(config['color'].get(bool))
|
||||
|
||||
# Compatibility from list_format_{item,album} to format_{item,album}
|
||||
for elem in ('item', 'album'):
|
||||
old_key = 'list_format_{0}'.format(elem)
|
||||
if config[old_key].exists():
|
||||
new_key = 'format_{0}'.format(elem)
|
||||
log.warning('Warning: configuration uses "{0}" which is deprecated'
|
||||
' in favor of "{1}" now that it affects all commands. '
|
||||
'See changelog & documentation.'.format(old_key,
|
||||
new_key))
|
||||
config[new_key].set(config[old_key])
|
||||
|
||||
config_path = config.user_config_path()
|
||||
if os.path.isfile(config_path):
|
||||
log.debug(u'user configuration: {0}',
|
||||
|
|
@ -913,6 +1038,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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -139,8 +139,6 @@ class ConvertPlugin(BeetsPlugin):
|
|||
cmd = ui.Subcommand('convert', help='convert to external location')
|
||||
cmd.parser.add_option('-p', '--pretend', action='store_true',
|
||||
help='show actions but do nothing')
|
||||
cmd.parser.add_option('-a', '--album', action='store_true',
|
||||
help='choose albums instead of tracks')
|
||||
cmd.parser.add_option('-t', '--threads', action='store', type='int',
|
||||
help='change the number of threads, \
|
||||
defaults to maximum available processors')
|
||||
|
|
@ -148,11 +146,12 @@ class ConvertPlugin(BeetsPlugin):
|
|||
dest='keep_new', help='keep only the converted \
|
||||
and move the old files')
|
||||
cmd.parser.add_option('-d', '--dest', action='store',
|
||||
help='set the destination directory')
|
||||
help='set the target format of the tracks')
|
||||
cmd.parser.add_option('-f', '--format', action='store', dest='format',
|
||||
help='set the destination directory')
|
||||
cmd.parser.add_option('-y', '--yes', action='store_true', dest='yes',
|
||||
help='do not ask for confirmation')
|
||||
cmd.parser.add_album_option()
|
||||
cmd.func = self.convert_func
|
||||
return [cmd]
|
||||
|
||||
|
|
@ -359,7 +358,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
|
||||
|
|
|
|||
|
|
@ -125,17 +125,6 @@ class DuplicatesPlugin(BeetsPlugin):
|
|||
self._command = Subcommand('duplicates',
|
||||
help=__doc__,
|
||||
aliases=['dup'])
|
||||
|
||||
self._command.parser.add_option('-f', '--format', dest='format',
|
||||
action='store', type='string',
|
||||
help='print with custom format',
|
||||
metavar='FMT', default='')
|
||||
|
||||
self._command.parser.add_option('-a', '--album', dest='album',
|
||||
action='store_true',
|
||||
help='show duplicate albums instead of'
|
||||
' tracks')
|
||||
|
||||
self._command.parser.add_option('-c', '--count', dest='count',
|
||||
action='store_true',
|
||||
help='show duplicate counts')
|
||||
|
|
@ -168,15 +157,11 @@ class DuplicatesPlugin(BeetsPlugin):
|
|||
action='store', metavar='DEST',
|
||||
help='copy items to dest')
|
||||
|
||||
self._command.parser.add_option('-p', '--path', dest='path',
|
||||
action='store_true',
|
||||
help='print paths for matched items or'
|
||||
' albums')
|
||||
|
||||
self._command.parser.add_option('-t', '--tag', dest='tag',
|
||||
action='store',
|
||||
help='tag matched items with \'k=v\''
|
||||
' attribute')
|
||||
self._command.parser.add_all_common_options()
|
||||
|
||||
def commands(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -486,10 +486,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
|
|||
'-t', '--threshold', dest='threshold', action='store',
|
||||
type='float', default=0.15, help='Set difference threshold'
|
||||
)
|
||||
sim_cmd.parser.add_option(
|
||||
'-f', '--format', action='store', default='${difference}: ${path}',
|
||||
help='print with custom format'
|
||||
)
|
||||
sim_cmd.parser.add_format_option()
|
||||
|
||||
def sim_func(lib, opts, args):
|
||||
self.config.set_args(opts)
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
cmd.parser.add_option('-W', '--nowrite', action='store_false',
|
||||
default=config['import']['write'], dest='write',
|
||||
help="don't write updated metadata to files")
|
||||
cmd.parser.add_option('-f', '--format', action='store', default='',
|
||||
help='print with custom format')
|
||||
cmd.parser.add_format_option()
|
||||
cmd.func = self.func
|
||||
return [cmd]
|
||||
|
||||
|
|
@ -64,17 +63,16 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
pretend = opts.pretend
|
||||
write = opts.write
|
||||
query = ui.decargs(args)
|
||||
fmt = opts.format
|
||||
|
||||
self.singletons(lib, query, move, pretend, write, fmt)
|
||||
self.albums(lib, query, move, pretend, write, fmt)
|
||||
self.singletons(lib, query, move, pretend, write)
|
||||
self.albums(lib, query, move, pretend, write)
|
||||
|
||||
def singletons(self, lib, query, move, pretend, write, fmt):
|
||||
def singletons(self, lib, query, move, pretend, write):
|
||||
"""Retrieve and apply info from the autotagger for items matched by
|
||||
query.
|
||||
"""
|
||||
for item in lib.items(query + ['singleton:true']):
|
||||
item_formatted = format(item, fmt)
|
||||
item_formatted = format(item)
|
||||
if not item.mb_trackid:
|
||||
self._log.info(u'Skipping singleton with no mb_trackid: {0}',
|
||||
item_formatted)
|
||||
|
|
@ -93,13 +91,13 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
autotag.apply_item_metadata(item, track_info)
|
||||
apply_item_changes(lib, item, move, pretend, write)
|
||||
|
||||
def albums(self, lib, query, move, pretend, write, fmt):
|
||||
def albums(self, lib, query, move, pretend, write):
|
||||
"""Retrieve and apply info from the autotagger for albums matched by
|
||||
query and their items.
|
||||
"""
|
||||
# Process matching albums.
|
||||
for a in lib.albums(query):
|
||||
album_formatted = format(a, fmt)
|
||||
album_formatted = format(a)
|
||||
if not a.mb_albumid:
|
||||
self._log.info(u'Skipping album with no mb_albumid: {0}',
|
||||
album_formatted)
|
||||
|
|
|
|||
|
|
@ -94,19 +94,13 @@ class MissingPlugin(BeetsPlugin):
|
|||
self._command = Subcommand('missing',
|
||||
help=__doc__,
|
||||
aliases=['miss'])
|
||||
|
||||
self._command.parser.add_option('-f', '--format', dest='format',
|
||||
action='store', type='string',
|
||||
help='print with custom FORMAT',
|
||||
metavar='FORMAT', default='')
|
||||
|
||||
self._command.parser.add_option('-c', '--count', dest='count',
|
||||
action='store_true',
|
||||
help='count missing tracks per album')
|
||||
|
||||
self._command.parser.add_option('-t', '--total', dest='total',
|
||||
action='store_true',
|
||||
help='count total of missing tracks')
|
||||
self._command.add_format_option()
|
||||
|
||||
def commands(self):
|
||||
def _miss(lib, opts, args):
|
||||
|
|
|
|||
|
|
@ -42,11 +42,7 @@ class PlayPlugin(BeetsPlugin):
|
|||
'play',
|
||||
help='send music to a player as a playlist'
|
||||
)
|
||||
play_command.parser.add_option(
|
||||
'-a', '--album',
|
||||
action='store_true', default=False,
|
||||
help='query and load albums rather than tracks'
|
||||
)
|
||||
play_command.parser.add_album_option()
|
||||
play_command.func = self.play_music
|
||||
return [play_command]
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ from itertools import groupby
|
|||
|
||||
def random_item(lib, opts, args):
|
||||
query = decargs(args)
|
||||
fmt = '$path' if opts.path else opts.format
|
||||
|
||||
if opts.album:
|
||||
objs = list(lib.albums(query))
|
||||
|
|
@ -63,20 +62,15 @@ def random_item(lib, opts, args):
|
|||
objs = random.sample(objs, number)
|
||||
|
||||
for item in objs:
|
||||
print_(format(item, fmt))
|
||||
print_(format(item))
|
||||
|
||||
random_cmd = Subcommand('random',
|
||||
help='chose a random track or album')
|
||||
random_cmd.parser.add_option('-a', '--album', action='store_true',
|
||||
help='choose an album instead of track')
|
||||
random_cmd.parser.add_option('-p', '--path', action='store_true',
|
||||
help='print the path of the matched item')
|
||||
random_cmd.parser.add_option('-f', '--format', action='store',
|
||||
help='print with custom format', default='')
|
||||
random_cmd.parser.add_option('-n', '--number', action='store', type="int",
|
||||
help='number of objects to choose', default=1)
|
||||
random_cmd.parser.add_option('-e', '--equal-chance', action='store_true',
|
||||
help='each artist has the same chance')
|
||||
random_cmd.parser.add_all_common_options()
|
||||
random_cmd.func = random_item
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -867,7 +867,6 @@ class ReplayGainPlugin(BeetsPlugin):
|
|||
self.handle_track(item, write)
|
||||
|
||||
cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain')
|
||||
cmd.parser.add_option('-a', '--album', action='store_true',
|
||||
help='analyze albums instead of tracks')
|
||||
cmd.parser.add_album_option()
|
||||
cmd.func = func
|
||||
return [cmd]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ Changelog
|
|||
|
||||
Features:
|
||||
|
||||
* Beets now accept top-level options ``--format-item`` and ``--format-album``
|
||||
before any subcommand to control how items and albums are displayed.
|
||||
:bug:`1271`:
|
||||
* :doc:`/plugins/replaygain`: There is a new backend for the `bs1770gain`_
|
||||
tool. Thanks to :user:`jmwatte`. :bug:`1343`
|
||||
* There are now multiple levels of verbosity. On the command line, you can
|
||||
|
|
@ -69,10 +72,13 @@ Core changes:
|
|||
``albumtotal`` computed attribute that provides the total number of tracks
|
||||
on the album. (The :ref:`per_disc_numbering` option has no influence on this
|
||||
field.)
|
||||
* The :ref:`list_format_album` and :ref:`list_format_item` configuration keys
|
||||
* The `list_format_album` and `list_format_item` configuration keys
|
||||
now affect (almost) every place where objects are printed and logged.
|
||||
(Previously, they only controlled the :ref:`list-cmd` command and a few
|
||||
other scattered pieces.) :bug:`1269`
|
||||
* `list_format_album` and `list_format_album` have respectively been
|
||||
renamed :ref:`format_album` and :ref:`format_item`. The old names still work
|
||||
but each triggers a warning message. :bug:`1271`
|
||||
|
||||
Fixes:
|
||||
|
||||
|
|
@ -120,6 +126,9 @@ Fixes:
|
|||
|
||||
For developers:
|
||||
|
||||
* the ``OptionParser`` is now a ``CommonOptionsParser`` that offers facilities
|
||||
for adding usual options (``--album``, ``--path`` and ``--format``). See
|
||||
:ref:`add_subcommands`. :bug:`1271`
|
||||
* The logging system in beets has been overhauled. Plugins now each have their
|
||||
own logger, which helps by automatically adjusting the verbosity level in
|
||||
import mode and by prefixing the plugin's name. Logging levels are
|
||||
|
|
|
|||
|
|
@ -87,8 +87,11 @@ The function should use any of the utility functions defined in ``beets.ui``.
|
|||
Try running ``pydoc beets.ui`` to see what's available.
|
||||
|
||||
You can add command-line options to your new command using the ``parser`` member
|
||||
of the ``Subcommand`` class, which is an ``OptionParser`` instance. Just use it
|
||||
like you would a normal ``OptionParser`` in an independent script.
|
||||
of the ``Subcommand`` class, which is a ``CommonOptionParser`` instance. Just
|
||||
use it like you would a normal ``OptionParser`` in an independent script. Note
|
||||
that it offers several methods to add common options: ``--album``, ``--path``
|
||||
and ``--format``. This feature is versatile and extensively documented, try
|
||||
``pydoc beets.ui.CommonOptionParser`` for more information.
|
||||
|
||||
.. _plugin_events:
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ file. The available options mirror the command-line options:
|
|||
or album. This uses the same template syntax as beets'
|
||||
:doc:`path formats</reference/pathformat>`. The usage is inspired by, and
|
||||
therefore similar to, the :ref:`list <list-cmd>` command.
|
||||
Default: :ref:`list_format_item`
|
||||
Default: :ref:`format_item`
|
||||
- **full**: List every track or album that has duplicates, not just the
|
||||
duplicates themselves.
|
||||
Default: ``no``.
|
||||
|
|
|
|||
|
|
@ -34,5 +34,5 @@ The command has a few command-line options:
|
|||
plugin will write new metadata to files' tags. To disable this, use the
|
||||
``-W`` (``--nowrite``) option.
|
||||
* To customize the output of unrecognized items, use the ``-f``
|
||||
(``--format``) option. The default output is ``list_format_item`` or
|
||||
``list_format_album`` for items and albums, respectively.
|
||||
(``--format``) option. The default output is ``format_item`` or
|
||||
``format_album`` for items and albums, respectively.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ configuration file. The available options are:
|
|||
track. This uses the same template syntax as beets'
|
||||
:doc:`path formats </reference/pathformat>`. The usage is inspired by, and
|
||||
therefore similar to, the :ref:`list <list-cmd>` command.
|
||||
Default: :ref:`list_format_item`.
|
||||
Default: :ref:`format_item`.
|
||||
- **total**: Print a single count of missing tracks in all albums.
|
||||
Default: ``no``.
|
||||
|
||||
|
|
|
|||
|
|
@ -162,25 +162,32 @@ Either ``yes`` or ``no``, indicating whether the autotagger should use
|
|||
multiple threads. This makes things faster but may behave strangely.
|
||||
Defaults to ``yes``.
|
||||
|
||||
.. _list_format_item:
|
||||
|
||||
list_format_item
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. _list_format_item:
|
||||
.. _format_item:
|
||||
|
||||
format_item
|
||||
~~~~~~~~~~~
|
||||
|
||||
Format to use when listing *individual items* with the :ref:`list-cmd`
|
||||
command and other commands that need to print out items. Defaults to
|
||||
``$artist - $album - $title``. The ``-f`` command-line option overrides
|
||||
this setting.
|
||||
|
||||
.. _list_format_album:
|
||||
It used to be named `list_format_item`.
|
||||
|
||||
list_format_album
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. _list_format_album:
|
||||
.. _format_album:
|
||||
|
||||
format_album
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Format to use when listing *albums* with :ref:`list-cmd` and other
|
||||
commands. Defaults to ``$albumartist - $album``. The ``-f`` command-line
|
||||
option overrides this setting.
|
||||
|
||||
It used to be named `list_format_album`.
|
||||
|
||||
.. _sort_item:
|
||||
|
||||
sort_item
|
||||
|
|
|
|||
|
|
@ -988,7 +988,7 @@ class TemplateTest(_common.LibTestCase):
|
|||
self.assertEqual(self.i.evaluate_template('$foo'), 'baz')
|
||||
|
||||
def test_album_and_item_format(self):
|
||||
config['list_format_album'] = u'foö $foo'
|
||||
config['format_album'] = u'foö $foo'
|
||||
album = beets.library.Album()
|
||||
album.foo = 'bar'
|
||||
album.tagada = 'togodo'
|
||||
|
|
@ -997,7 +997,7 @@ class TemplateTest(_common.LibTestCase):
|
|||
self.assertEqual(unicode(album), u"foö bar")
|
||||
self.assertEqual(str(album), b"fo\xc3\xb6 bar")
|
||||
|
||||
config['list_format_item'] = 'bar $foo'
|
||||
config['format_item'] = 'bar $foo'
|
||||
item = beets.library.Item()
|
||||
item.foo = 'bar'
|
||||
item.tagada = 'togodo'
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ class MbsyncCliTest(unittest.TestCase, TestHelper):
|
|||
self.assertEqual(album.album, 'album info')
|
||||
|
||||
def test_message_when_skipping(self):
|
||||
config['list_format_item'] = '$artist - $album - $title'
|
||||
config['list_format_album'] = '$albumartist - $album'
|
||||
config['format_item'] = '$artist - $album - $title'
|
||||
config['format_album'] = '$albumartist - $album'
|
||||
|
||||
# Test album with no mb_albumid.
|
||||
# The default format for an album include $albumartist so
|
||||
|
|
@ -99,6 +99,10 @@ class MbsyncCliTest(unittest.TestCase, TestHelper):
|
|||
e = "mbsync: Skipping album with no mb_albumid: 'album info'"
|
||||
self.assertEqual(e, logs[0])
|
||||
|
||||
# restore the config
|
||||
config['format_item'] = '$artist - $album - $title'
|
||||
config['format_album'] = '$albumartist - $album'
|
||||
|
||||
# Test singleton with no mb_trackid.
|
||||
# The default singleton format includes $artist and $album
|
||||
# so we need to stub them here
|
||||
|
|
|
|||
139
test/test_ui.py
139
test/test_ui.py
|
|
@ -1033,6 +1033,145 @@ class CompletionTest(_common.TestCase):
|
|||
self.fail('test/test_completion.sh did not execute properly')
|
||||
|
||||
|
||||
class CommonOptionsParserCliTest(unittest.TestCase, TestHelper):
|
||||
"""Test CommonOptionsParser and formatting LibModel formatting on 'list'
|
||||
command.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
self.lib = library.Library(':memory:')
|
||||
self.item = _common.item()
|
||||
self.item.path = 'xxx/yyy'
|
||||
self.lib.add(self.item)
|
||||
self.lib.add_album([self.item])
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
|
||||
def test_base(self):
|
||||
l = self.run_with_output('ls')
|
||||
self.assertEqual(l, 'the artist - the album - the title\n')
|
||||
|
||||
l = self.run_with_output('ls', '-a')
|
||||
self.assertEqual(l, 'the album artist - the album\n')
|
||||
|
||||
def test_path_option(self):
|
||||
l = self.run_with_output('ls', '-p')
|
||||
self.assertEqual(l, 'xxx/yyy\n')
|
||||
|
||||
l = self.run_with_output('ls', '-a', '-p')
|
||||
self.assertEqual(l, 'xxx\n')
|
||||
|
||||
def test_format_option(self):
|
||||
l = self.run_with_output('ls', '-f', '$artist')
|
||||
self.assertEqual(l, 'the artist\n')
|
||||
|
||||
l = self.run_with_output('ls', '-a', '-f', '$albumartist')
|
||||
self.assertEqual(l, 'the album artist\n')
|
||||
|
||||
def test_root_format_option(self):
|
||||
l = self.run_with_output('--format-item', '$artist',
|
||||
'--format-album', 'foo', 'ls')
|
||||
self.assertEqual(l, 'the artist\n')
|
||||
|
||||
l = self.run_with_output('--format-item', 'foo',
|
||||
'--format-album', '$albumartist', 'ls', '-a')
|
||||
self.assertEqual(l, 'the album artist\n')
|
||||
|
||||
|
||||
class CommonOptionsParserTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
|
||||
def test_album_option(self):
|
||||
parser = ui.CommonOptionsParser()
|
||||
self.assertFalse(parser._album_flags)
|
||||
parser.add_album_option()
|
||||
self.assertTrue(bool(parser._album_flags))
|
||||
|
||||
self.assertEqual(parser.parse_args([]), ({'album': None}, []))
|
||||
self.assertEqual(parser.parse_args(['-a']), ({'album': True}, []))
|
||||
self.assertEqual(parser.parse_args(['--album']), ({'album': True}, []))
|
||||
|
||||
def test_path_option(self):
|
||||
parser = ui.CommonOptionsParser()
|
||||
parser.add_path_option()
|
||||
self.assertFalse(parser._album_flags)
|
||||
|
||||
config['format_item'].set('$foo')
|
||||
self.assertEqual(parser.parse_args([]), ({'path': None}, []))
|
||||
self.assertEqual(config['format_item'].get(unicode), u'$foo')
|
||||
|
||||
self.assertEqual(parser.parse_args(['-p']),
|
||||
({'path': True, 'format': '$path'}, []))
|
||||
self.assertEqual(parser.parse_args(['--path']),
|
||||
({'path': True, 'format': '$path'}, []))
|
||||
|
||||
self.assertEqual(config['format_item'].get(unicode), '$path')
|
||||
self.assertEqual(config['format_album'].get(unicode), '$path')
|
||||
|
||||
def test_format_option(self):
|
||||
parser = ui.CommonOptionsParser()
|
||||
parser.add_format_option()
|
||||
self.assertFalse(parser._album_flags)
|
||||
|
||||
config['format_item'].set('$foo')
|
||||
self.assertEqual(parser.parse_args([]), ({'format': None}, []))
|
||||
self.assertEqual(config['format_item'].get(unicode), u'$foo')
|
||||
|
||||
self.assertEqual(parser.parse_args(['-f', '$bar']),
|
||||
({'format': '$bar'}, []))
|
||||
self.assertEqual(parser.parse_args(['--format', '$baz']),
|
||||
({'format': '$baz'}, []))
|
||||
|
||||
self.assertEqual(config['format_item'].get(unicode), '$baz')
|
||||
self.assertEqual(config['format_album'].get(unicode), '$baz')
|
||||
|
||||
def test_format_option_with_target(self):
|
||||
with self.assertRaises(KeyError):
|
||||
ui.CommonOptionsParser().add_format_option(target='thingy')
|
||||
|
||||
parser = ui.CommonOptionsParser()
|
||||
parser.add_format_option(target='item')
|
||||
|
||||
config['format_item'].set('$item')
|
||||
config['format_album'].set('$album')
|
||||
|
||||
self.assertEqual(parser.parse_args(['-f', '$bar']),
|
||||
({'format': '$bar'}, []))
|
||||
|
||||
self.assertEqual(config['format_item'].get(unicode), '$bar')
|
||||
self.assertEqual(config['format_album'].get(unicode), '$album')
|
||||
|
||||
def test_format_option_with_album(self):
|
||||
parser = ui.CommonOptionsParser()
|
||||
parser.add_album_option()
|
||||
parser.add_format_option()
|
||||
|
||||
config['format_item'].set('$item')
|
||||
config['format_album'].set('$album')
|
||||
|
||||
parser.parse_args(['-f', '$bar'])
|
||||
self.assertEqual(config['format_item'].get(unicode), '$bar')
|
||||
self.assertEqual(config['format_album'].get(unicode), '$album')
|
||||
|
||||
parser.parse_args(['-a', '-f', '$foo'])
|
||||
self.assertEqual(config['format_item'].get(unicode), '$bar')
|
||||
self.assertEqual(config['format_album'].get(unicode), '$foo')
|
||||
|
||||
parser.parse_args(['-f', '$foo2', '-a'])
|
||||
self.assertEqual(config['format_album'].get(unicode), '$foo2')
|
||||
|
||||
def test_add_all_common_options(self):
|
||||
parser = ui.CommonOptionsParser()
|
||||
parser.add_all_common_options()
|
||||
self.assertEqual(parser.parse_args([]),
|
||||
({'album': None, 'path': None, 'format': None}, []))
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue