mirror of
https://github.com/beetbox/beets.git
synced 2026-01-02 14:03:12 +01:00
Merge remote-tracking branch 'origin/patch-2' into patch-1
This commit is contained in:
commit
c91c91c7da
13 changed files with 128 additions and 93 deletions
|
|
@ -36,3 +36,5 @@ notifications:
|
|||
- "irc.freenode.org#beets"
|
||||
use_notice: true
|
||||
skip_join: true
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue