diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 7221af22d..64837b8b0 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -471,7 +471,7 @@ def tag_item(item, search_artist=None, search_title=None, hooks.TrackMatch(dist, track_info) # If this is a good match, then don't keep searching. rec = recommendation(candidates.values()) - if rec == RECOMMEND_STRONG and not config['import_timid'].get(bool): + if rec == RECOMMEND_STRONG and not config['import']['timid']: log.debug('Track ID match.') return candidates.values(), rec diff --git a/beets/importer.py b/beets/importer.py index 277f648fd..3411c4b72 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -628,7 +628,7 @@ def initial_lookup(session): if task.sentinel: continue - plugins.send('import_task_start', task=task) + plugins.send('import_task_start', session=session, task=task) log.debug('Looking up: %s' % task.path) try: @@ -654,7 +654,7 @@ def user_query(session): choice = session.choose_match(task) task.set_choice(choice) session.log_choice(task) - plugins.send('import_task_choice', task=task) + plugins.send('import_task_choice', session=session, task=task) # As-tracks: transition to singleton workflow. if choice is action.TRACKS: @@ -726,7 +726,7 @@ def apply_choices(session): ) else: autotag.apply_item_metadata(task.item, task.match.info) - plugins.send('import_task_apply', task=task) + plugins.send('import_task_apply', session=session, task=task) # Infer album-level fields. if task.is_album: @@ -852,7 +852,7 @@ def manipulate_files(session): session.lib.store(item) # Plugin event. - plugins.send('import_task_files', task=task) + plugins.send('import_task_files', session=session, task=task) def finalize(session): """A coroutine that finishes up importer tasks. In particular, the @@ -908,7 +908,7 @@ def item_lookup(session): if task.sentinel: continue - plugins.send('import_task_start', task=task) + plugins.send('import_task_start', session=session, task=task) task.set_item_candidates(*autotag.tag_item(task.item)) @@ -926,7 +926,7 @@ def item_query(session): choice = session.choose_item(task) task.set_choice(choice) session.log_choice(task) - plugins.send('import_task_choice', task=task) + plugins.send('import_task_choice', session=session, task=task) # Duplicate check. if task.choice_flag in (action.ASIS, action.APPLY): diff --git a/beetsplug/bench.py b/beetsplug/bench.py index b5b89db2d..082fa6d4e 100644 --- a/beetsplug/bench.py +++ b/beetsplug/bench.py @@ -56,7 +56,7 @@ class BenchmarkPlugin(BeetsPlugin): """A plugin for performing some simple performance benchmarks. """ def commands(self): - def bench_func(lib, config, opts, args): + def bench_func(lib, opts, args): benchmark(lib, opts.profile) bench_cmd = ui.Subcommand('bench', help='benchmark') bench_cmd.parser.add_option('-p', '--profile', diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index b921aa377..89fbc68cf 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -29,12 +29,17 @@ import beets from beets.plugins import BeetsPlugin import beets.ui from beets import vfs +from beets import config from beets.util import bluelet +config.add({ + 'bpd': { + 'host': u'', + 'port': 6600, + 'password': u'', + } +}) -DEFAULT_PORT = 6600 -DEFAULT_HOST = '' -DEFAULT_PASSWORD = '' PROTOCOL_VERSION = '0.13.0' BUFSIZE = 1024 @@ -154,8 +159,7 @@ class BaseServer(object): This is a generic superclass and doesn't support many commands. """ - def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, - password=DEFAULT_PASSWORD): + def __init__(self, host, port, password): """Create a new server bound to address `host` and listening on port `port`. If `password` is given, it is required to do anything significant on the server. @@ -727,7 +731,7 @@ class Server(BaseServer): to store its library. """ - def __init__(self, library, host='', port=DEFAULT_PORT, password=''): + def __init__(self, library, host, port, password): try: from beetsplug.bpd import gstplayer except ImportError as e: @@ -1144,15 +1148,12 @@ class BPDPlugin(BeetsPlugin): cmd.parser.add_option('-d', '--debug', action='store_true', help='dump all MPD traffic to stdout') - def func(lib, config, opts, args): - host = args.pop(0) if args else \ - beets.ui.config_val(config, 'bpd', 'host', DEFAULT_HOST) - port = args.pop(0) if args else \ - beets.ui.config_val(config, 'bpd', 'port', str(DEFAULT_PORT)) + def func(lib, opts, args): + host = args.pop(0) if args else config['bpd']['host'].get(unicode) + port = args.pop(0) if args else config['bpd']['port'].get(int) if args: raise beets.ui.UserError('too many arguments') - password = beets.ui.config_val(config, 'bpd', 'password', - DEFAULT_PASSWORD) + password = config['bpd']['password'].get(unicode) debug = opts.debug or False self.start_bpd(lib, host, int(port), password, debug) diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index e2eb4af3a..53cdf82b2 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -18,6 +18,8 @@ autotagger. Requires the pyacoustid library. from beets import plugins from beets import ui from beets import util +from beets import config +from beets.util import confit from beets.autotag import hooks import acoustid import logging @@ -43,9 +45,6 @@ _matches = {} _fingerprints = {} _acoustids = {} -# The user's Acoustid API key, if provided. -_userkey = None - def acoustid_match(path): """Gets metadata for a file from Acoustid and populates the @@ -148,17 +147,15 @@ class AcoustidPlugin(plugins.BeetsPlugin): log.debug('acoustid item candidates: {0}'.format(len(tracks))) return tracks - def configure(self, config): - global _userkey - _userkey = ui.config_val(config, 'acoustid', 'apikey', None) - def commands(self): submit_cmd = ui.Subcommand('submit', help='submit Acoustid fingerprints') - def submit_cmd_func(lib, config, opts, args): - if not _userkey: + def submit_cmd_func(lib, opts, args): + try: + apikey = config['acoustid']['apikey'].get(unicode) + except confit.NotFoundError: raise ui.UserError('no Acoustid user API key provided') - submit_items(_userkey, lib.items(ui.decargs(args))) + submit_items(apikey, lib.items(ui.decargs(args))) submit_cmd.func = submit_cmd_func return [submit_cmd] @@ -166,7 +163,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): # Hooks into import process. @AcoustidPlugin.listen('import_task_start') -def fingerprint_task(config=None, task=None): +def fingerprint_task(task): """Fingerprint each item in the task for later use during the autotagging candidate search. """ @@ -175,7 +172,7 @@ def fingerprint_task(config=None, task=None): acoustid_match(item.path) @AcoustidPlugin.listen('import_task_apply') -def apply_acoustid_metadata(config=None, task=None): +def apply_acoustid_metadata(task): """Apply Acoustid metadata (fingerprint and ID) to the task's items. """ for item in task.imported_items(): diff --git a/beetsplug/convert.py b/beetsplug/convert.py index b9259d2fb..d52d2dc3c 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -16,25 +16,37 @@ """ import logging import os -import shutil import threading -from subprocess import Popen, PIPE +from subprocess import Popen from beets.plugins import BeetsPlugin from beets import ui, util from beetsplug.embedart import _embed +from beets import config log = logging.getLogger('beets') DEVNULL = open(os.devnull, 'wb') -conf = {} _fs_lock = threading.Lock() +config.add({ + 'convert': { + u'dest': None, + u'threads': util.cpu_count(), + u'ffmpeg': u'ffmpeg', + u'opts': u'-aq 2', + u'max_bitrate': 500, + u'embed': True, + } +}) + def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) - encode = Popen([conf['ffmpeg']] + ['-i', source] + conf['opts'] + - [dest], close_fds=True, stderr=DEVNULL) + opts = config['convert']['opts'].get(unicode).split(u' ') + encode = Popen([config['convert']['ffmpeg'].get(unicode), '-i', source] + + opts + [dest], + close_fds=True, stderr=DEVNULL) encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files @@ -64,7 +76,8 @@ def convert_item(lib, dest_dir): with _fs_lock: util.mkdirall(dest) - if item.format == 'MP3' and item.bitrate < 1000 * conf['max_bitrate']: + maxbr = config['convert']['max_bitrate'].get(int) + if item.format == 'MP3' and item.bitrate < 1000 * maxbr: log.info(u'Copying {0}'.format(util.displayable_path(item.path))) util.copy(item.path, dest) else: @@ -74,17 +87,19 @@ def convert_item(lib, dest_dir): item.write() artpath = lib.get_album(item).artpath - if artpath and conf['embed']: + if artpath and config['convert']['embed']: _embed(artpath, [item]) -def convert_func(lib, config, opts, args): - dest = opts.dest if opts.dest is not None else conf['dest'] +def convert_func(lib, opts, args): + dest = opts.dest if opts.dest is not None else \ + config['convert']['dest'].get() if not dest: raise ui.UserError('no convert destination set') - threads = opts.threads if opts.threads is not None else conf['threads'] + threads = opts.threads if opts.threads is not None else \ + config['convert']['threads'].get(int) - ui.commands.list_items(lib, ui.decargs(args), opts.album, None, config) + ui.commands.list_items(lib, ui.decargs(args), opts.album, None) if not ui.input_yn("Convert? (Y/n)"): return @@ -99,18 +114,6 @@ def convert_func(lib, config, opts, args): class ConvertPlugin(BeetsPlugin): - def configure(self, config): - conf['dest'] = ui.config_val(config, 'convert', 'dest', None) - conf['threads'] = int(ui.config_val(config, 'convert', 'threads', - util.cpu_count())) - conf['ffmpeg'] = ui.config_val(config, 'convert', 'ffmpeg', 'ffmpeg') - conf['opts'] = ui.config_val(config, 'convert', - 'opts', '-aq 2').split(' ') - conf['max_bitrate'] = int(ui.config_val(config, 'convert', - 'max_bitrate', '500')) - conf['embed'] = ui.config_val(config, 'convert', 'embed', True, - vtype=bool) - def commands(self): cmd = ui.Subcommand('convert', help='convert to external location') cmd.parser.add_option('-a', '--album', action='store_true', diff --git a/beetsplug/echonest_tempo.py b/beetsplug/echonest_tempo.py index df9ff16b4..9406ae0ef 100644 --- a/beetsplug/echonest_tempo.py +++ b/beetsplug/echonest_tempo.py @@ -19,15 +19,19 @@ import logging from beets.plugins import BeetsPlugin from beets import ui from beets.ui import commands +from beets import config import pyechonest.config import pyechonest.song # Global logger. log = logging.getLogger('beets') -# The official Echo Nest API key for beets. This can be overridden by -# the user. -ECHONEST_APIKEY = 'NY2KTZHQ0QDSHBAP6' +config.add({ + 'echonest_tempo': { + 'apikey': u'NY2KTZHQ0QDSHBAP6', + 'auto': True, + } +}) def fetch_item_tempo(lib, loglevel, item, write): """Fetch and store tempo for a single item. If ``write``, then the @@ -68,22 +72,23 @@ def get_tempo(artist, title): else: return None -AUTOFETCH = True class EchoNestTempoPlugin(BeetsPlugin): def __init__(self): super(EchoNestTempoPlugin, self).__init__() self.import_stages = [self.imported] + pyechonest.config.ECHO_NEST_API_KEY = \ + config['echonest_tempo']['apikey'].get(unicode) + def commands(self): cmd = ui.Subcommand('tempo', help='fetch song tempo (bpm)') cmd.parser.add_option('-p', '--print', dest='printbpm', action='store_true', default=False, help='print tempo (bpm) to console') - def func(lib, config, opts, args): + def func(lib, opts, args): # The "write to files" option corresponds to the # import_write config value. - write = ui.config_val(config, 'beets', 'import_write', - commands.DEFAULT_IMPORT_WRITE, bool) + write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): fetch_item_tempo(lib, logging.INFO, item, write) @@ -92,16 +97,8 @@ class EchoNestTempoPlugin(BeetsPlugin): cmd.func = func return [cmd] - def configure(self, config): - global AUTOFETCH - AUTOFETCH = ui.config_val(config, 'echonest_tempo', 'autofetch', True, - bool) - apikey = ui.config_val(config, 'echonest_tempo', 'apikey', - ECHONEST_APIKEY) - pyechonest.config.ECHO_NEST_API_KEY = apikey - # Auto-fetch tempo on import. def imported(self, config, task): - if AUTOFETCH: + if config['echonest_tempo']['auto']: for item in task.imported_items(): fetch_item_tempo(config.lib, logging.DEBUG, item, False) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py old mode 100755 new mode 100644 index 9aaef1965..c5625bb2e --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -22,14 +22,23 @@ from beets import ui from beets.ui import decargs from beets.util import syspath, normpath from beets.util.artresizer import ArtResizer +from beets import config log = logging.getLogger('beets') +config.add({ + 'embedart': { + 'maxwidth': 0, + 'auto': True, + } +}) + def _embed(path, items): """Embed an image file, located at `path`, into each item. """ - if options['maxwidth']: - path = ArtResizer.shared.resize(options['maxwidth'], syspath(path)) + maxwidth = config['embedart']['maxwidth'].get(int) + if maxwidth: + path = ArtResizer.shared.resize(maxwidth, syspath(path)) data = open(syspath(path), 'rb').read() kindstr = imghdr.what(None, data) @@ -51,29 +60,22 @@ def _embed(path, items): f.art = data f.save() -options = { - 'autoembed': True, - 'maxwidth': 0, -} class EmbedCoverArtPlugin(BeetsPlugin): """Allows albumart to be embedded into the actual files. """ - def configure(self, config): - options['autoembed'] = \ - ui.config_val(config, 'embedart', 'autoembed', True, bool) - options['maxwidth'] = \ - int(ui.config_val(config, 'embedart', 'maxwidth', '0')) - - if options['maxwidth'] and not ArtResizer.shared.local: - options['maxwidth'] = 0 - log.error("embedart: ImageMagick or PIL not found; " - "'maxwidth' option ignored") + def __init__(self): + super(EmbedCoverArtPlugin, self).__init__() + if config['embedart']['maxwidth'].get(int) and \ + not ArtResizer.shared.local: + config['embedart']['maxwidth'] = 0 + log.warn("embedart: ImageMagick or PIL not found; " + "'maxwidth' option ignored") def commands(self): # Embed command. embed_cmd = ui.Subcommand('embedart', help='embed an image file into file metadata') - def embed_func(lib, config, opts, args): + def embed_func(lib, opts, args): if not args: raise ui.UserError('specify an image file') imagepath = normpath(args.pop(0)) @@ -85,7 +87,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): help='extract an image from file metadata') extract_cmd.parser.add_option('-o', dest='outpath', help='image output file') - def extract_func(lib, config, opts, args): + def extract_func(lib, opts, args): outpath = normpath(opts.outpath or 'cover') extract(lib, outpath, decargs(args)) extract_cmd.func = extract_func @@ -93,7 +95,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): # Clear command. clear_cmd = ui.Subcommand('clearart', help='remove images from file metadata') - def clear_func(lib, config, opts, args): + def clear_func(lib, opts, args): clear(lib, decargs(args)) clear_cmd.func = clear_func @@ -155,6 +157,6 @@ def clear(lib, query): # Automatically embed art into imported albums. @EmbedCoverArtPlugin.listen('album_imported') -def album_imported(lib, album, config): - if album.artpath and options['autoembed']: +def album_imported(lib, album): + if album.artpath and config['embedart']['auto']: _embed(album.artpath, album.items()) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 57299dca9..9c8185f53 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -25,6 +25,7 @@ from beets.util.artresizer import ArtResizer from beets import importer from beets import ui from beets import util +from beets import config IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg'] COVER_NAMES = ['cover', 'front', 'art', 'album', 'folder'] @@ -33,6 +34,13 @@ DOWNLOAD_EXTENSION = '.jpg' log = logging.getLogger('beets') +config.add({ + 'fetchart': { + 'auto': True, + 'maxwidth': 0, + } +}) + def _fetch_image(url): """Downloads an image from a URL and checks whether it seems to @@ -214,18 +222,15 @@ class FetchArtPlugin(BeetsPlugin): # placing them in the filesystem. self.art_paths = {} - def configure(self, config): - self.autofetch = ui.config_val(config, 'fetchart', - 'autofetch', True, bool) - self.maxwidth = int(ui.config_val(config, 'fetchart', - 'maxwidth', '0')) + self.autofetch = config['fetchart']['auto'].get(bool) + self.maxwidth = config['fetchart']['maxwidth'].get(int) if self.autofetch: # Enable two import hooks when fetching is enabled. self.import_stages = [self.fetch_art] self.register_listener('import_task_files', self.assign_art) # Asynchronous; after music is added to the library. - def fetch_art(self, config, task): + def fetch_art(self, session, task): """Find art for the album being imported.""" if task.is_album: # Only fetch art for full albums. if task.choice_flag == importer.action.ASIS: @@ -238,22 +243,23 @@ class FetchArtPlugin(BeetsPlugin): # For any other choices (e.g., TRACKS), do nothing. return - album = config.lib.get_album(task.album_id) + album = session.lib.get_album(task.album_id) path = art_for_album(album, task.path, self.maxwidth, local) if path: self.art_paths[task] = path # Synchronous; after music files are put in place. - def assign_art(self, config, task): + def assign_art(self, session, task): """Place the discovered art in the filesystem.""" if task in self.art_paths: path = self.art_paths.pop(task) - album = config.lib.get_album(task.album_id) - album.set_art(path, not (config.delete or config.move)) - - if config.delete or config.move: + album = session.lib.get_album(task.album_id) + src_removed = config['import']['delete'].get(bool) or \ + config['import']['move'].get(bool) + album.set_art(path, not src_removed) + if src_removed: task.prune(path) # Manual album art fetching. @@ -262,7 +268,7 @@ class FetchArtPlugin(BeetsPlugin): cmd.parser.add_option('-f', '--force', dest='force', action='store_true', default=False, help='re-download art when already present') - def func(lib, config, opts, args): + def func(lib, opts, args): batch_fetch_art(lib, lib.albums(ui.decargs(args)), opts.force, self.maxwidth) cmd.func = func diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py old mode 100755 new mode 100644 diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py old mode 100755 new mode 100644