mirror of
https://github.com/beetbox/beets.git
synced 2025-12-27 02:52:33 +01:00
preliminary reorganization of CLI code; remove cmdln module dependency
This commit is contained in:
parent
e50a26e8a4
commit
2ca121cb03
3 changed files with 309 additions and 155 deletions
190
beet
190
beet
|
|
@ -14,168 +14,50 @@
|
|||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
import cmdln
|
||||
import ConfigParser
|
||||
import os
|
||||
import sys
|
||||
import optparse
|
||||
|
||||
from beets import ui
|
||||
from beets import Library
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
'beets': {
|
||||
'library': '~/.beetsmusic.blb',
|
||||
'directory': '~/Music',
|
||||
'path_format': '$artist/$album/$track $title',
|
||||
'import_copy': True,
|
||||
'import_write': True,
|
||||
},
|
||||
|
||||
'bpd': {
|
||||
'host': '',
|
||||
'port': '6600',
|
||||
'password': '',
|
||||
},
|
||||
}
|
||||
|
||||
CONFIG_FILE = os.path.expanduser('~/.beetsconfig')
|
||||
|
||||
def make_query(criteria):
|
||||
"""Make query string for the list of criteria."""
|
||||
return ' '.join(criteria).strip() or None
|
||||
|
||||
class BeetsApp(cmdln.Cmdln):
|
||||
name = "beet"
|
||||
|
||||
def get_optparser(self):
|
||||
# Add global options to the command.
|
||||
parser = cmdln.Cmdln.get_optparser(self)
|
||||
parser.add_option('-l', '--library', dest='libpath',
|
||||
help='library database file to use')
|
||||
parser.add_option('-d', '--directory', dest='directory',
|
||||
help="destination music directory")
|
||||
parser.add_option('-p', '--pathformat', dest='path_format',
|
||||
help="destination path format string")
|
||||
parser.add_option('-i', '--device', dest='device',
|
||||
help="name of the device library to use")
|
||||
return parser
|
||||
|
||||
def postoptparse(self):
|
||||
# Read defaults from config file.
|
||||
self.config = ConfigParser.SafeConfigParser()
|
||||
self.config.read(CONFIG_FILE)
|
||||
for sec in CONFIG_DEFAULTS:
|
||||
if not self.config.has_section(sec):
|
||||
self.config.add_section(sec)
|
||||
|
||||
# Open library file.
|
||||
if self.options.device:
|
||||
from beets.device import PodLibrary
|
||||
self.lib = PodLibrary.by_name(self.options.device)
|
||||
else:
|
||||
libpath = self.options.libpath or \
|
||||
self._cfg_get('beets', 'library')
|
||||
directory = self.options.directory or \
|
||||
self._cfg_get('beets', 'directory')
|
||||
path_format = self.options.path_format or \
|
||||
self._cfg_get('beets', 'path_format')
|
||||
self.lib = Library(os.path.expanduser(libpath),
|
||||
directory,
|
||||
path_format)
|
||||
|
||||
def _cfg_get(self, section, name, vtype=None):
|
||||
try:
|
||||
if vtype is bool:
|
||||
return self.config.getboolean(section, name)
|
||||
else:
|
||||
return self.config.get(section, name)
|
||||
except ConfigParser.NoOptionError:
|
||||
return CONFIG_DEFAULTS[section][name]
|
||||
|
||||
@cmdln.alias("imp", "im")
|
||||
@cmdln.option('-c', '--copy', action='store_true', default=None,
|
||||
help="copy tracks into library directory (default)")
|
||||
@cmdln.option('-C', '--nocopy', action='store_false', dest='copy',
|
||||
help="don't copy tracks (opposite of -c)")
|
||||
@cmdln.option('-w', '--write', action='store_true', default=None,
|
||||
help="write new metadata to files' tags (default)")
|
||||
@cmdln.option('-W', '--nowrite', action='store_false', dest='write',
|
||||
help="don't write metadata (opposite of -s)")
|
||||
@cmdln.option('-a', '--autotag', action='store_true', dest='autotag',
|
||||
help="infer tags for imported files (default)")
|
||||
@cmdln.option('-A', '--noautotag', action='store_false', dest='autotag',
|
||||
help="don't infer tags for imported files (opposite of -a)")
|
||||
@cmdln.option('-l', '--log', dest='logpath',
|
||||
help='file to log untaggable albums for later review')
|
||||
def do_import(self, subcmd, opts, *paths):
|
||||
"""${cmd_name}: import new music
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
copy = opts.copy if opts.copy is not None else \
|
||||
self._cfg_get('beets', 'import_copy', bool)
|
||||
write = opts.write if opts.write is not None else \
|
||||
self._cfg_get('beets', 'import_write', bool)
|
||||
autot = opts.autotag if opts.autotag is not None else True
|
||||
ui.import_files(self.lib, paths, copy, write, autot, opts.logpath)
|
||||
|
||||
@cmdln.alias("ls")
|
||||
@cmdln.option('-a', '--album', action='store_true',
|
||||
help='show matching albums instead of tracks')
|
||||
def do_list(self, subcmd, opts, *criteria):
|
||||
"""${cmd_name}: query the library
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
ui.list_items(self.lib, make_query(criteria), opts.album)
|
||||
|
||||
@cmdln.alias("rm")
|
||||
@cmdln.option("-d", "--delete", action="store_true",
|
||||
help="also remove files from disk")
|
||||
@cmdln.option('-a', '--album', action='store_true',
|
||||
help='match albums instead of tracks')
|
||||
def do_remove(self, subcmd, opts, *criteria):
|
||||
"""${cmd_name}: remove matching items from the library
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
q = make_query(criteria)
|
||||
ui.remove_items(self.lib, make_query(criteria),
|
||||
opts.album, opts.delete)
|
||||
|
||||
@cmdln.option('-d', '--debug', action='store_true',
|
||||
help='dump all MPD traffic to stdout')
|
||||
def do_bpd(self, subcmd, opts, host=None, port=None):
|
||||
"""${cmd_name}: run an MPD-compatible music player server
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
host = host or self._cfg_get('bpd', 'host')
|
||||
port = port or self._cfg_get('bpd', 'port')
|
||||
password = self._cfg_get('bpd', 'password')
|
||||
debug = opts.debug or False
|
||||
ui.start_bpd(self.lib, host, int(port), password, debug)
|
||||
|
||||
def do_dadd(self, subcmd, opts, name, *criteria):
|
||||
"""${cmd_name}: add files to a device
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
ui.device_add(self.lib, make_query(criteria), name)
|
||||
|
||||
def do_stats(self, subcmd, opts, *criteria):
|
||||
"""${cmd_name}: show statistics about the library or a query
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
ui.show_stats(self.lib, make_query(criteria))
|
||||
parser = ui.SubcommandsOptionParser(subcommands=ui.default_subcommands)
|
||||
parser.add_option('-l', '--library', dest='libpath',
|
||||
help='library database file to use')
|
||||
parser.add_option('-d', '--directory', dest='directory',
|
||||
help="destination music directory")
|
||||
parser.add_option('-p', '--pathformat', dest='path_format',
|
||||
help="destination path format string")
|
||||
parser.add_option('-i', '--device', dest='device',
|
||||
help="name of the device library to use")
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = BeetsApp()
|
||||
sys.exit(app.main())
|
||||
options, subcommand, suboptions, subargs = parser.parse_args()
|
||||
|
||||
# Read defaults from config file.
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.read(CONFIG_FILE)
|
||||
for sec in ui.CONFIG_DEFAULTS:
|
||||
if not config.has_section(sec):
|
||||
config.add_section(sec)
|
||||
|
||||
# Open library file.
|
||||
if options.device:
|
||||
from beets.device import PodLibrary
|
||||
lib = PodLibrary.by_name(self.options.device)
|
||||
else:
|
||||
libpath = options.libpath or \
|
||||
ui._cfg_get(config, 'beets', 'library')
|
||||
directory = options.directory or \
|
||||
ui._cfg_get(config, 'beets', 'directory')
|
||||
path_format = options.path_format or \
|
||||
ui._cfg_get(config, 'beets', 'path_format')
|
||||
lib = Library(os.path.expanduser(libpath),
|
||||
directory,
|
||||
path_format)
|
||||
|
||||
# XXX
|
||||
subcommand.func(lib, config, suboptions, subargs)
|
||||
|
|
|
|||
273
beets/ui.py
273
beets/ui.py
|
|
@ -15,6 +15,9 @@
|
|||
import os
|
||||
import logging
|
||||
import locale
|
||||
import optparse
|
||||
import textwrap
|
||||
import ConfigParser
|
||||
|
||||
from beets import autotag
|
||||
from beets import library
|
||||
|
|
@ -24,6 +27,7 @@ from beets.player import bpd
|
|||
# Global logger.
|
||||
log = logging.getLogger('beets')
|
||||
|
||||
|
||||
# Utilities.
|
||||
|
||||
def _print(txt=''):
|
||||
|
|
@ -128,6 +132,164 @@ def _human_seconds(interval):
|
|||
return "%3.1f %ss" % (interval, suffix)
|
||||
|
||||
|
||||
# Subcommand parsing infrastructure.
|
||||
|
||||
# This is a fairly generic subcommand parser for optparse. It is
|
||||
# maintained externally here:
|
||||
# http://gist.github.com/462717
|
||||
# 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.
|
||||
"""
|
||||
def __init__(self, name, parser, help='', aliases=()):
|
||||
"""Creates a new subcommand. name is the primary way to invoke
|
||||
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."""
|
||||
self.name = name
|
||||
self.parser = parser
|
||||
self.aliases = aliases
|
||||
self.help = help
|
||||
|
||||
class SubcommandsOptionParser(optparse.OptionParser):
|
||||
"""A variant of OptionParser that parses subcommands and their
|
||||
arguments.
|
||||
"""
|
||||
|
||||
# A singleton command used to give help on other subcommands.
|
||||
_HelpSubcommand = Subcommand('help', optparse.OptionParser(),
|
||||
help='give detailed help on a specific sub-command',
|
||||
aliases=('?',))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Create a new subcommand-aware option parser. All of the
|
||||
options to OptionParser.__init__ are supported in addition
|
||||
to subcommands, a sequence of Subcommand objects.
|
||||
"""
|
||||
# The subcommand array, with the help command included.
|
||||
self.subcommands = list(kwargs.pop('subcommands', []))
|
||||
self.subcommands.append(self._HelpSubcommand)
|
||||
|
||||
# A more helpful default usage.
|
||||
if 'usage' not in kwargs:
|
||||
kwargs['usage'] = """
|
||||
%prog COMMAND [ARGS...]
|
||||
%prog help COMMAND"""
|
||||
|
||||
# Super constructor.
|
||||
optparse.OptionParser.__init__(self, *args, **kwargs)
|
||||
|
||||
# Adjust the help-visible name of each subcommand.
|
||||
for subcommand in self.subcommands:
|
||||
subcommand.parser.prog = '%s %s' % \
|
||||
(self.get_prog_name(), subcommand.name)
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
self.disable_interspersed_args()
|
||||
|
||||
# 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)
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
|
||||
# Subcommands header.
|
||||
result = ["\n"]
|
||||
result.append(formatter.format_heading('Commands'))
|
||||
formatter.indent()
|
||||
|
||||
# Generate the display names (including aliases).
|
||||
# Also determine the help position.
|
||||
disp_names = []
|
||||
help_position = 0
|
||||
for subcommand in self.subcommands:
|
||||
name = subcommand.name
|
||||
if subcommand.aliases:
|
||||
name += ' (%s)' % ', '.join(subcommand.aliases)
|
||||
disp_names.append(name)
|
||||
|
||||
# Set the help position based on the max width.
|
||||
proposed_help_position = len(name) + formatter.current_indent + 2
|
||||
if proposed_help_position <= formatter.max_help_position:
|
||||
help_position = max(help_position, proposed_help_position)
|
||||
|
||||
# Add each subcommand to the output.
|
||||
for subcommand, name in zip(self.subcommands, disp_names):
|
||||
# Lifted directly from optparse.py.
|
||||
name_width = help_position - formatter.current_indent - 2
|
||||
if len(name) > name_width:
|
||||
name = "%*s%s\n" % (formatter.current_indent, "", name)
|
||||
indent_first = help_position
|
||||
else:
|
||||
name = "%*s%-*s " % (formatter.current_indent, "",
|
||||
name_width, name)
|
||||
indent_first = 0
|
||||
result.append(name)
|
||||
help_width = formatter.width - help_position
|
||||
help_lines = textwrap.wrap(subcommand.help, help_width)
|
||||
result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
|
||||
result.extend(["%*s%s\n" % (help_position, "", line)
|
||||
for line in help_lines[1:]])
|
||||
formatter.dedent()
|
||||
|
||||
# Concatenate the original help message with the subcommand
|
||||
# list.
|
||||
return out + "".join(result)
|
||||
|
||||
def _subcommand_for_name(self, name):
|
||||
"""Return the subcommand in self.subcommands matching the
|
||||
given name. The name may either be the name of a subcommand or
|
||||
an alias. If no subcommand matches, returns None.
|
||||
"""
|
||||
for subcommand in self.subcommands:
|
||||
if name == subcommand.name or \
|
||||
name in subcommand.aliases:
|
||||
return subcommand
|
||||
return None
|
||||
|
||||
def parse_args(self, a=None, v=None):
|
||||
"""Like OptionParser.parse_args, but returns these four items:
|
||||
- options: the options passed to the root parser
|
||||
- subcommand: the Subcommand object that was invoked
|
||||
- suboptions: the options passed to the subcommand parser
|
||||
- subargs: the positional arguments passed to the subcommand
|
||||
"""
|
||||
options, args = optparse.OptionParser.parse_args(self, a, v)
|
||||
|
||||
if not args:
|
||||
# No command given.
|
||||
self.print_help()
|
||||
self.exit()
|
||||
else:
|
||||
cmdname = args.pop(0)
|
||||
if cmdname.startswith('-'):
|
||||
parser.error('unknown option ' + cmdname)
|
||||
else:
|
||||
subcommand = self._subcommand_for_name(cmdname)
|
||||
if not subcommand:
|
||||
parser.error('unknown command ' + cmdname)
|
||||
|
||||
suboptions, subargs = subcommand.parser.parse_args(args)
|
||||
|
||||
if subcommand is self._HelpSubcommand:
|
||||
if subargs:
|
||||
# particular
|
||||
cmdname = subargs[0]
|
||||
helpcommand = self._subcommand_for_name(cmdname)
|
||||
helpcommand.parser.print_help()
|
||||
self.exit()
|
||||
else:
|
||||
# general
|
||||
self.print_help()
|
||||
self.exit()
|
||||
|
||||
return options, subcommand, suboptions, subargs
|
||||
|
||||
|
||||
# Autotagging interface.
|
||||
|
||||
def show_change(cur_artist, cur_album, items, info, dist):
|
||||
|
|
@ -499,3 +661,114 @@ Albums: %i""" % (
|
|||
_human_bytes(total_size),
|
||||
len(artists), len(albums)
|
||||
))
|
||||
|
||||
|
||||
# XXX
|
||||
CONFIG_DEFAULTS = {
|
||||
'beets': {
|
||||
'library': '~/.beetsmusic.blb',
|
||||
'directory': '~/Music',
|
||||
'path_format': '$artist/$album/$track $title',
|
||||
'import_copy': True,
|
||||
'import_write': True,
|
||||
},
|
||||
|
||||
'bpd': {
|
||||
'host': '',
|
||||
'port': '6600',
|
||||
'password': '',
|
||||
},
|
||||
}
|
||||
def _cfg_get(config, section, name, vtype=None):
|
||||
try:
|
||||
if vtype is bool:
|
||||
return config.getboolean(section, name)
|
||||
else:
|
||||
return config.get(section, name)
|
||||
except ConfigParser.NoOptionError:
|
||||
return CONFIG_DEFAULTS[section][name]
|
||||
def make_query(criteria):
|
||||
"""Make query string for the list of criteria."""
|
||||
return ' '.join(criteria).strip() or None
|
||||
|
||||
|
||||
|
||||
# Default subcommands.
|
||||
|
||||
default_subcommands = []
|
||||
|
||||
import_cmd = Subcommand('import', optparse.OptionParser(),
|
||||
'import new music', ('imp', 'im'))
|
||||
import_cmd.parser.add_option('-c', '--copy', action='store_true',
|
||||
default=None, help="copy tracks into library directory (default)")
|
||||
import_cmd.parser.add_option('-C', '--nocopy', action='store_false',
|
||||
dest='copy', help="don't copy tracks (opposite of -c)")
|
||||
import_cmd.parser.add_option('-w', '--write', action='store_true',
|
||||
default=None, help="write new metadata to files' tags (default)")
|
||||
import_cmd.parser.add_option('-W', '--nowrite', action='store_false',
|
||||
dest='write', help="don't write metadata (opposite of -s)")
|
||||
import_cmd.parser.add_option('-a', '--autotag', action='store_true',
|
||||
dest='autotag', help="infer tags for imported files (default)")
|
||||
import_cmd.parser.add_option('-A', '--noautotag', action='store_false',
|
||||
dest='autotag',
|
||||
help="don't infer tags for imported files (opposite of -a)")
|
||||
import_cmd.parser.add_option('-l', '--log', dest='logpath',
|
||||
help='file to log untaggable albums for later review')
|
||||
def import_func(lib, config, opts, args):
|
||||
copy = opts.copy if opts.copy is not None else \
|
||||
self._cfg_get('beets', 'import_copy', bool)
|
||||
write = opts.write if opts.write is not None else \
|
||||
self._cfg_get('beets', 'import_write', bool)
|
||||
autot = opts.autotag if opts.autotag is not None else True
|
||||
import_files(lib, args, copy, write, autot, opts.logpath)
|
||||
import_cmd.func = import_func
|
||||
default_subcommands.append(import_cmd)
|
||||
|
||||
list_cmd = Subcommand('list', optparse.OptionParser(),
|
||||
'query the library', ('ls',))
|
||||
list_cmd.parser.add_option('-a', '--album', action='store_true',
|
||||
help='show matching albums instead of tracks')
|
||||
def list_func(lib, config, opts, args):
|
||||
list_items(lib, make_query(args), opts.album)
|
||||
list_cmd.func = list_func
|
||||
default_subcommands.append(list_cmd)
|
||||
|
||||
remove_cmd = Subcommand('remove', optparse.OptionParser(),
|
||||
'remove matching items from the library', ('rm',))
|
||||
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')
|
||||
def remove_func(lib, config, opts, args):
|
||||
remove_items(lib, make_query(args), opts.album, opts.delete)
|
||||
remove_cmd.func = remove_func
|
||||
default_subcommands.append(remove_cmd)
|
||||
|
||||
bpd_cmd = Subcommand('bpd', optparse.OptionParser(),
|
||||
'run an MPD-compatible music player server')
|
||||
bpd_cmd.parser.add_option('-d', '--debug', action='store_true',
|
||||
help='dump all MPD traffic to stdout')
|
||||
def bpd_func(lib, config, opts, args):
|
||||
host = args.pop(0) if args else _cfg_get(config, 'bpd', 'host')
|
||||
port = args.pop(0) if args else _cfg_get(config, 'bpd', 'port')
|
||||
password = _cfg_get(config, 'bpd', 'password')
|
||||
debug = opts.debug or False
|
||||
start_bpd(lib, host, int(port), password, debug)
|
||||
bpd_cmd.func = bpd_func
|
||||
default_subcommands.append(bpd_cmd)
|
||||
|
||||
dadd_cmd = Subcommand('dadd', optparse.OptionParser(),
|
||||
'add files to a device')
|
||||
def dadd_func(lib, config, opts, args):
|
||||
name = args.pop(0)
|
||||
# fixme require exactly one arg
|
||||
device_add(lib, make_query(args), name)
|
||||
dadd_cmd.func = dadd_func
|
||||
default_subcommands.append(dadd_cmd)
|
||||
|
||||
stats_cmd = Subcommand('stats', optparse.OptionParser(),
|
||||
'show statistics about the library or a query')
|
||||
def stats_func(lib, config, opts, args):
|
||||
show_stats(lib, make_query(args))
|
||||
stats_cmd.func = stats_func
|
||||
default_subcommands.append(stats_cmd)
|
||||
1
setup.py
1
setup.py
|
|
@ -49,7 +49,6 @@ setup(name='beets',
|
|||
'mutagen',
|
||||
'python-musicbrainz2 >= 0.7.0',
|
||||
'munkres',
|
||||
'cmdln',
|
||||
'eventlet >= 0.9.3',
|
||||
],
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue