diff --git a/.travis.yml b/.travis.yml
index 2e635d6f5..4907a7cb1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -36,3 +36,5 @@ notifications:
- "irc.freenode.org#beets"
use_notice: true
skip_join: true
+ on_success: change
+ on_failure: always
diff --git a/beets/importer.py b/beets/importer.py
index a050be27c..296ac99ba 100644
--- a/beets/importer.py
+++ b/beets/importer.py
@@ -950,8 +950,6 @@ def read_tasks(session):
.format(displayable_path(dirs)))
skipped += 1
continue
- print(paths)
- print(read_items(paths))
yield ImportTask(toppath, dirs, read_items(paths))
# Indicate the directory is finished.
diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py
index 829089200..d235cc758 100644
--- a/beets/ui/__init__.py
+++ b/beets/ui/__init__.py
@@ -499,14 +499,6 @@ def get_plugin_paths():
The value for "pluginpath" may be a single string or a list of
strings.
"""
- pluginpaths = config['pluginpath'].get()
- if isinstance(pluginpaths, basestring):
- pluginpaths = [pluginpaths]
- if not isinstance(pluginpaths, list):
- raise confit.ConfigTypeError(
- u'pluginpath must be string or a list of strings'
- )
- return map(util.normpath, pluginpaths)
def _pick_format(album, fmt=None):
@@ -694,6 +686,16 @@ class SubcommandsOptionParser(optparse.OptionParser):
# Super constructor.
optparse.OptionParser.__init__(self, *args, **kwargs)
+ self.add_option('-l', '--library', dest='library',
+ help='library database file to use')
+ self.add_option('-d', '--directory', dest='directory',
+ help="destination music directory")
+ self.add_option('-v', '--verbose', dest='verbose', action='store_true',
+ help='print debugging information')
+ self.add_option('-c', '--config', dest='config',
+ help='path to configuration file')
+ self.add_option('-h', '--help', dest='help', action='store_true',
+ help='how this help message and exit')
# Our root parser needs to stop on the first unrecognized argument.
self.disable_interspersed_args()
@@ -840,26 +842,34 @@ def vararg_callback(option, opt_str, value, parser):
# The main entry point and bootstrapping.
-def _load_plugins():
+def _load_plugins(config):
"""Load the plugins specified in the configuration.
"""
- # Add plugin paths.
+ paths = config['pluginpath'].get(confit.EnsureStringList())
+ paths = map(util.normpath, paths)
+
import beetsplug
- beetsplug.__path__ = get_plugin_paths() + beetsplug.__path__
-
+ beetsplug.__path__ = paths + beetsplug.__path__
# For backwards compatibility.
- sys.path += get_plugin_paths()
+ sys.path += paths
- # Load requested plugins.
plugins.load_plugins(config['plugins'].as_str_seq())
plugins.send("pluginload")
+ return plugins
-def _configure(args):
- """Parse the command line, load configuration files (including
- loading any indicated plugins), and return the invoked subcomand,
- the subcommand options, and the subcommand arguments.
+def _setup(options, lib=None):
+ """Prepare and global state and updates it with command line options.
+
+ Returns a list of subcommands, a list of plugins, and a library instance.
"""
+ # Configure the MusicBrainz API.
+ mb.configure()
+
+ config = _configure(options)
+
+ plugins = _load_plugins(config)
+
# Temporary: Migrate from 1.0-style configuration.
from beets.ui import migrate
migrate.automigrate()
@@ -867,21 +877,20 @@ def _configure(args):
# Get the default subcommands.
from beets.ui.commands import default_commands
- # Construct the root parser.
- parser = SubcommandsOptionParser()
- parser.add_option('-l', '--library', dest='library',
- help='library database file to use')
- parser.add_option('-d', '--directory', dest='directory',
- help="destination music directory")
- parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
- help='print debugging information')
- parser.add_option('-c', '--config', dest='config',
- help='path to configuration file')
- parser.add_option('-h', '--help', dest='help', action='store_true',
- help='how this help message and exit')
+ subcommands = list(default_commands)
+ subcommands.append(migrate.migrate_cmd)
+ subcommands.extend(plugins.commands())
- # Parse the command-line!
- options, subargs = parser.parse_global_options(args)
+ if lib is None:
+ lib = _open_library(config)
+ plugins.send("library_opened", lib=lib)
+
+ return subcommands, plugins, lib
+
+
+def _configure(options):
+ """Amend the global configuration object with command line options.
+ """
# Add any additional config files specified with --config. This
# special handling lets specified plugins get loaded before we
@@ -906,54 +915,48 @@ def _configure(args):
log.debug('no user configuration found at {0}'.format(
util.displayable_path(config_path)))
- # Add builtin subcommands
- parser.add_subcommand(*default_commands)
- parser.add_subcommand(migrate.migrate_cmd)
+ log.debug(u'data directory: {0}'
+ .format(util.displayable_path(config.config_dir())))
+ return config
- # Now add the plugin commands to the parser.
- _load_plugins()
- for cmd in plugins.commands():
- parser.add_subcommand(cmd)
- # Parse the remainder of the command line with loaded plugins.
- return parser.parse_subcommand(subargs)
+def _open_library(config):
+ """Create a new library instance from the configuration.
+ """
+ dbpath = config['library'].as_filename()
+ try:
+ lib = library.Library(
+ dbpath,
+ config['directory'].as_filename(),
+ get_path_formats(),
+ get_replacements(),
+ )
+ except sqlite3.OperationalError:
+ raise UserError(u"database file {0} could not be opened".format(
+ util.displayable_path(dbpath)
+ ))
+ log.debug(u'library database: {0}\n'
+ u'library directory: {1}'
+ .format(
+ util.displayable_path(lib.path),
+ util.displayable_path(lib.directory),
+ ))
+ return lib
def _raw_main(args, lib=None):
"""A helper function for `main` without top-level exception
handling.
"""
- subcommand, suboptions, subargs = _configure(args)
- if lib is None:
- # Open library file.
- dbpath = config['library'].as_filename()
- try:
- lib = library.Library(
- dbpath,
- config['directory'].as_filename(),
- get_path_formats(),
- get_replacements(),
- )
- except sqlite3.OperationalError:
- raise UserError(u"database file {0} could not be opened".format(
- util.displayable_path(dbpath)
- ))
- plugins.send("library_opened", lib=lib)
+ parser = SubcommandsOptionParser()
+ options, subargs = parser.parse_global_options(args)
- log.debug(u'data directory: {0}\n'
- u'library database: {1}\n'
- u'library directory: {2}'
- .format(
- util.displayable_path(config.config_dir()),
- util.displayable_path(lib.path),
- util.displayable_path(lib.directory),
- ))
+ subcommands, plugins, lib = _setup(options, lib)
- # Configure the MusicBrainz API.
- mb.configure()
+ parser.add_subcommand(*subcommands)
+ subcommand, suboptions, subargs = parser.parse_subcommand(subargs)
- # Invoke the subcommand.
subcommand.func(lib, suboptions, subargs)
plugins.send('cli_exit', lib=lib)
diff --git a/beets/util/confit.py b/beets/util/confit.py
index 0149f68a7..4ffe68cea 100644
--- a/beets/util/confit.py
+++ b/beets/util/confit.py
@@ -1073,6 +1073,22 @@ class StrSeq(Template):
self.fail('must be a list of strings', view, True)
+class EnsureStringList(Template):
+ """Always return a list of strings.
+
+ The raw value may either be a single string or a list of strings.
+ Otherwise a type error is raised. For single strings a singleton
+ list is returned.
+ """
+ def convert(self, paths, view):
+ if isinstance(paths, basestring):
+ paths = [paths]
+ if not isinstance(paths, list) or \
+ not all(map(lambda p: isinstance(p, basestring), paths)):
+ self.fail(u'must be string or a list of strings', view, True)
+ return paths
+
+
class Filename(Template):
"""A template that validates strings as filenames.
diff --git a/beetsplug/info.py b/beetsplug/info.py
index 9fd6f4f12..f7462761a 100644
--- a/beetsplug/info.py
+++ b/beetsplug/info.py
@@ -60,8 +60,7 @@ def run(lib, opts, args):
else:
if not first:
ui.print_()
- else:
- print_data(data)
+ print_data(data)
first = False
if opts.summarize:
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index 01a188616..186e83d6e 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -122,6 +122,7 @@ def strip_cruft(lyrics, wscollapse=True):
lyrics = unescape(lyrics)
if wscollapse:
lyrics = re.sub(r'\s+', ' ', lyrics) # Whitespace collapse.
+ lyrics = re.sub(r'<(script).*?\1>(?s)', '', lyrics) # Strip script tags.
lyrics = BREAK_RE.sub('\n', lyrics) #
newlines.
lyrics = re.sub(r'\n +', '\n', lyrics)
lyrics = re.sub(r' +\n', '\n', lyrics)
diff --git a/beetsplug/play.py b/beetsplug/play.py
index 8c04228cc..9a1bc444c 100644
--- a/beetsplug/play.py
+++ b/beetsplug/play.py
@@ -19,6 +19,7 @@ from beets.ui import Subcommand
from beets import config
from beets import ui
from beets import util
+from os.path import relpath
import platform
import logging
import shlex
@@ -33,6 +34,9 @@ def play_music(lib, opts, args):
"""
command_str = config['play']['command'].get()
use_folders = config['play']['use_folders'].get(bool)
+ relative_to = config['play']['relative_to'].get()
+ if relative_to:
+ relative_to = util.normpath(relative_to)
if command_str:
command = shlex.split(command_str)
else:
@@ -86,7 +90,10 @@ def play_music(lib, opts, args):
# Create temporary m3u file to hold our playlist.
m3u = NamedTemporaryFile('w', suffix='.m3u', delete=False)
for item in paths:
- m3u.write(item + '\n')
+ if relative_to:
+ m3u.write(relpath(item, relative_to) + '\n')
+ else:
+ m3u.write(item + '\n')
m3u.close()
command.append(m3u.name)
@@ -96,7 +103,7 @@ def play_music(lib, opts, args):
if output:
log.debug(u'Output of {0}: {1}'.format(command[0], output))
- ui.print_(u'Playing {0} {1}.'.format(len(paths), item_type))
+ ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type))
class PlayPlugin(BeetsPlugin):
diff --git a/docs/changelog.rst b/docs/changelog.rst
index b435dcd41..c67a38472 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -37,6 +37,10 @@ Fixes:
data when the two are inconsistent.
* Resuming imports and beginning incremental imports should now be much faster
when there is a lot of previously-imported music to skip.
+* :doc:`/plugins/lyrics`: Remove ``