Merge remote-tracking branch 'origin/patch-2' into patch-1

This commit is contained in:
Thomas Gordon 2014-09-06 14:00:33 -04:00
commit c91c91c7da
13 changed files with 128 additions and 93 deletions

View file

@ -36,3 +36,5 @@ notifications:
- "irc.freenode.org#beets"
use_notice: true
skip_join: true
on_success: change
on_failure: always

View file

@ -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.

View file

@ -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)

View file

@ -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.

View file

@ -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:

View file

@ -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) # <BR> newlines.
lyrics = re.sub(r'\n +', '\n', lyrics)
lyrics = re.sub(r' +\n', '\n', lyrics)

View file

@ -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):

View file

@ -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 ``<script>`` tags from scraped lyrics. Thanks
to Bombardment.
* :doc:`/plugins/play`: Add a ``relative_to`` config option. Thanks to
BrainDamage.
.. _discogs_client: https://github.com/discogs/discogs_client

View file

@ -23,6 +23,12 @@ would on the command-line)::
play:
command: /usr/bin/command --option1 --option2 some_other_option
You can configure the plugin to emit relative paths. Use the ``relative_to``
configuration option::
play:
relative_to: /my/music/folder
When using the ``-a`` option, the m3u will have the paths to each track on
the matched albums. If you wish to have folders instead, you can change that
by setting ``use_files: False`` in your configuration file.

View file

@ -45,7 +45,7 @@ if 'sdist' in sys.argv:
setup(
name='beets',
version='1.3.7',
version='1.3.8',
description='music tagger and library organizer',
author='Adrian Sampson',
author_email='adrian@radbox.org',

View file

@ -17,9 +17,9 @@
import os
import sqlite3
import _common
from _common import unittest
from beets import dbcore
from tempfile import mkstemp
# Fixture: concrete database and model classes. For migration tests, we
@ -105,15 +105,14 @@ class TestDatabaseTwoModels(dbcore.Database):
pass
class MigrationTest(_common.TestCase):
class MigrationTest(unittest.TestCase):
"""Tests the ability to change the database schema between
versions.
"""
def setUp(self):
super(MigrationTest, self).setUp()
handle, self.libfile = mkstemp('db')
os.close(handle)
# Set up a database with the two-field schema.
self.libfile = os.path.join(self.temp_dir, 'temp.db')
old_lib = TestDatabase2(self.libfile)
# Add an item to the old library.
@ -123,6 +122,9 @@ class MigrationTest(_common.TestCase):
old_lib._connection().commit()
del old_lib
def tearDown(self):
os.remove(self.libfile)
def test_open_with_same_fields_leaves_untouched(self):
new_lib = TestDatabase2(self.libfile)
c = new_lib._connection().cursor()
@ -159,15 +161,12 @@ class MigrationTest(_common.TestCase):
self.fail("select failed")
class ModelTest(_common.TestCase):
class ModelTest(unittest.TestCase):
def setUp(self):
super(ModelTest, self).setUp()
dbfile = os.path.join(self.temp_dir, 'temp.db')
self.db = TestDatabase1(dbfile)
self.db = TestDatabase1(':memory:')
def tearDown(self):
self.db._connection().close()
super(ModelTest, self).tearDown()
def test_add_model(self):
model = TestModel1()
@ -251,7 +250,7 @@ class ModelTest(_common.TestCase):
self.assertEqual(model.some_float_field, 0.0)
class FormatTest(_common.TestCase):
class FormatTest(unittest.TestCase):
def test_format_fixed_field(self):
model = TestModel1()
model.field_one = u'caf\xe9'
@ -283,7 +282,7 @@ class FormatTest(_common.TestCase):
self.assertEqual(value, u'3.1')
class FormattedMappingTest(_common.TestCase):
class FormattedMappingTest(unittest.TestCase):
def test_keys_equal_model_keys(self):
model = TestModel1()
formatted = model.formatted()
@ -306,7 +305,7 @@ class FormattedMappingTest(_common.TestCase):
self.assertEqual(formatted.get('other_field', 'default'), 'default')
class ParseTest(_common.TestCase):
class ParseTest(unittest.TestCase):
def test_parse_fixed_field(self):
value = TestModel1._parse('field_one', u'2')
self.assertIsInstance(value, int)
@ -322,7 +321,7 @@ class ParseTest(_common.TestCase):
self.assertEqual(value, u'2')
class QueryParseTest(_common.TestCase):
class QueryParseTest(unittest.TestCase):
def pqp(self, part):
return dbcore.queryparse.parse_query_part(
part,
@ -381,7 +380,7 @@ class QueryParseTest(_common.TestCase):
self.assertEqual(self.pqp(q), r)
class QueryFromStringsTest(_common.TestCase):
class QueryFromStringsTest(unittest.TestCase):
def qfs(self, strings):
return dbcore.queryparse.query_from_strings(
dbcore.query.AndQuery,
@ -412,7 +411,7 @@ class QueryFromStringsTest(_common.TestCase):
self.assertIsInstance(q.subqueries[0], dbcore.query.NumericQuery)
class SortFromStringsTest(_common.TestCase):
class SortFromStringsTest(unittest.TestCase):
def sfs(self, strings):
return dbcore.queryparse.sort_from_strings(
TestModel1,

View file

@ -70,7 +70,7 @@ class LastGenrePluginTest(unittest.TestCase, TestHelper):
"""
self._setup_config(canonical=True, whitelist=True, count=99)
self.assertEqual(self.plugin._resolve_genres(['delta blues']),
'Delta Blues, Country Blues, Blues')
'Delta Blues, Blues')
def test_whitelist_custom(self):
"""Keep only genres that are in the whitelist.

View file

@ -740,7 +740,7 @@ class ConfigTest(unittest.TestCase, TestHelper):
beetsdir = os.path.join(self.temp_dir, 'beetsfile')
open(beetsdir, 'a').close()
os.environ['BEETSDIR'] = beetsdir
self.assertRaises(ConfigError, ui._raw_main, 'test')
self.assertRaises(ConfigError, ui._raw_main, ['test'])
def test_beetsdir_config_does_not_load_default_user_config(self):
os.environ['BEETSDIR'] = self.beetsdir