mirror of
https://github.com/beetbox/beets.git
synced 2026-01-08 17:08:12 +01:00
begin moving importer/autotagger to confit
This commit is contained in:
parent
405390ac3a
commit
75d43270e8
6 changed files with 126 additions and 164 deletions
|
|
@ -18,7 +18,7 @@ import os
|
|||
import logging
|
||||
import re
|
||||
|
||||
from beets import library, mediafile
|
||||
from beets import library, mediafile, config
|
||||
from beets.util import sorted_walk, ancestry
|
||||
|
||||
# Parts of external interface.
|
||||
|
|
@ -115,7 +115,7 @@ def apply_item_metadata(item, track_info):
|
|||
# At the moment, the other metadata is left intact (including album
|
||||
# and track number). Perhaps these should be emptied?
|
||||
|
||||
def apply_metadata(album_info, mapping, per_disc_numbering=False):
|
||||
def apply_metadata(album_info, mapping):
|
||||
"""Set the items' metadata to match an AlbumInfo object using a
|
||||
mapping from Items to TrackInfo objects. If `per_disc_numbering`,
|
||||
then the track numbers are per-disc instead of per-release.
|
||||
|
|
@ -148,7 +148,7 @@ def apply_metadata(album_info, mapping, per_disc_numbering=False):
|
|||
# Title.
|
||||
item.title = track_info.title
|
||||
|
||||
if per_disc_numbering:
|
||||
if config['per_disc_numbering']:
|
||||
item.track = track_info.medium_index
|
||||
else:
|
||||
item.track = track_info.index
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from munkres import Munkres
|
|||
from unidecode import unidecode
|
||||
|
||||
from beets import plugins
|
||||
from beets import config
|
||||
from beets.util import levenshtein, plurality
|
||||
from beets.autotag import hooks
|
||||
|
||||
|
|
@ -373,7 +374,7 @@ def _add_candidate(items, results, info):
|
|||
results[info.album_id] = hooks.AlbumMatch(dist, info, mapping,
|
||||
extra_items, extra_tracks)
|
||||
|
||||
def tag_album(items, timid=False, search_artist=None, search_album=None,
|
||||
def tag_album(items, search_artist=None, search_album=None,
|
||||
search_id=None):
|
||||
"""Bundles together the functionality used to infer tags for a
|
||||
set of items comprised by an album. Returns everything relevant:
|
||||
|
|
@ -407,7 +408,7 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
_add_candidate(items, candidates, id_info)
|
||||
rec = recommendation(candidates.values())
|
||||
log.debug('Album ID match recommendation is ' + str(rec))
|
||||
if candidates and not timid:
|
||||
if candidates and not config['import']['timid']:
|
||||
# If we have a very good MBID match, return immediately.
|
||||
# Otherwise, this match will compete against metadata-based
|
||||
# matches.
|
||||
|
|
@ -446,7 +447,7 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
rec = recommendation(candidates)
|
||||
return cur_artist, cur_album, candidates, rec
|
||||
|
||||
def tag_item(item, timid=False, search_artist=None, search_title=None,
|
||||
def tag_item(item, search_artist=None, search_title=None,
|
||||
search_id=None):
|
||||
"""Attempts to find metadata for a single track. Returns a
|
||||
`(candidates, recommendation)` pair where `candidates` is a list of
|
||||
|
|
@ -469,7 +470,7 @@ def tag_item(item, timid=False, 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 timid:
|
||||
if rec == RECOMMEND_STRONG and not config['import_timid'].get(bool):
|
||||
log.debug('Track ID match.')
|
||||
return candidates.values(), rec
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
library: ~/.beetsmusic.blb
|
||||
directory: ~/Music
|
||||
|
||||
import_write: yes
|
||||
import_copy: yes
|
||||
import_move: no
|
||||
import_resume: ask
|
||||
import_incremental: yes
|
||||
import_quiet_fallback: skip
|
||||
import_timid: no
|
||||
import_log:
|
||||
import:
|
||||
write: yes
|
||||
copy: yes
|
||||
move: no
|
||||
resume: ask
|
||||
incremental: no
|
||||
quiet_fallback: skip
|
||||
timid: no
|
||||
log:
|
||||
autotag: yes
|
||||
quiet: no
|
||||
|
||||
# Typically only set from the command line.
|
||||
singletons: no
|
||||
|
||||
ignore: [".*", "*~"]
|
||||
replace:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from beets import autotag
|
|||
from beets import library
|
||||
from beets import plugins
|
||||
from beets import util
|
||||
from beets import config
|
||||
from beets.util import pipeline
|
||||
from beets.util import syspath, normpath, displayable_path
|
||||
from beets.util.enumeration import enum
|
||||
|
|
@ -451,13 +452,13 @@ class ImportTask(object):
|
|||
|
||||
# Full-album pipeline stages.
|
||||
|
||||
def read_tasks(config):
|
||||
def read_tasks():
|
||||
"""A generator yielding all the albums (as ImportTask objects) found
|
||||
in the user-specified list of paths. In the case of a singleton
|
||||
import, yields single-item tasks instead.
|
||||
"""
|
||||
# Look for saved progress.
|
||||
progress = config.resume is not False
|
||||
progress = config['import']['resume'] is not False # FIXME
|
||||
if progress:
|
||||
resume_dirs = {}
|
||||
for path in config.paths:
|
||||
|
|
@ -465,7 +466,7 @@ def read_tasks(config):
|
|||
if resume_dir:
|
||||
|
||||
# Either accept immediately or prompt for input to decide.
|
||||
if config.resume:
|
||||
if config['import']['resume']:
|
||||
do_resume = True
|
||||
log.warn('Resuming interrupted import of %s' % path)
|
||||
else:
|
||||
|
|
@ -478,13 +479,14 @@ def read_tasks(config):
|
|||
progress_set(path, None)
|
||||
|
||||
# Look for saved incremental directories.
|
||||
if config.incremental:
|
||||
if config['import']['incremental']:
|
||||
incremental_skipped = 0
|
||||
history_dirs = history_get()
|
||||
|
||||
for toppath in config.paths:
|
||||
# Check whether the path is to a file.
|
||||
if config.singletons and not os.path.isdir(syspath(toppath)):
|
||||
if config['import']['singletons'] and \
|
||||
not os.path.isdir(syspath(toppath)):
|
||||
item = library.Item.from_path(toppath)
|
||||
yield ImportTask.item_task(item)
|
||||
continue
|
||||
|
|
@ -503,14 +505,14 @@ def read_tasks(config):
|
|||
continue
|
||||
|
||||
# When incremental, skip paths in the history.
|
||||
if config.incremental and path in history_dirs:
|
||||
if config['import']['incremental'] and path in history_dirs:
|
||||
log.debug(u'Skipping previously-imported path: %s' %
|
||||
displayable_path(path))
|
||||
incremental_skipped += 1
|
||||
continue
|
||||
|
||||
# Yield all the necessary tasks.
|
||||
if config.singletons:
|
||||
if config['import']['singletons']:
|
||||
for item in items:
|
||||
yield ImportTask.item_task(item)
|
||||
yield ImportTask.progress_sentinel(toppath, path)
|
||||
|
|
@ -521,16 +523,16 @@ def read_tasks(config):
|
|||
yield ImportTask.done_sentinel(toppath)
|
||||
|
||||
# Show skipped directories.
|
||||
if config.incremental and incremental_skipped:
|
||||
if config['import']['incremental'] and incremental_skipped:
|
||||
log.info(u'Incremental import: skipped %i directories.' %
|
||||
incremental_skipped)
|
||||
|
||||
def query_tasks(config):
|
||||
def query_tasks():
|
||||
"""A generator that works as a drop-in-replacement for read_tasks.
|
||||
Instead of finding files from the filesystem, a query is used to
|
||||
match items from the library.
|
||||
"""
|
||||
if config.singletons:
|
||||
if config['import']['singletons']:
|
||||
# Search for items.
|
||||
for item in config.lib.items(config.query):
|
||||
yield ImportTask.item_task(item)
|
||||
|
|
@ -543,7 +545,7 @@ def query_tasks(config):
|
|||
items = list(album.items())
|
||||
yield ImportTask(None, album.item_dir(), items)
|
||||
|
||||
def initial_lookup(config):
|
||||
def initial_lookup():
|
||||
"""A coroutine for performing the initial MusicBrainz lookup for an
|
||||
album. It accepts lists of Items and yields
|
||||
(items, cur_artist, cur_album, candidates, rec) tuples. If no match
|
||||
|
|
@ -555,15 +557,16 @@ def initial_lookup(config):
|
|||
if task.sentinel:
|
||||
continue
|
||||
|
||||
plugins.send('import_task_start', task=task, config=config)
|
||||
plugins.send('import_task_start', task=task)
|
||||
|
||||
log.debug('Looking up: %s' % task.path)
|
||||
try:
|
||||
task.set_candidates(*autotag.tag_album(task.items, config.timid))
|
||||
task.set_candidates(*autotag.tag_album(task.items,
|
||||
config['import']['timid']))
|
||||
except autotag.AutotagError:
|
||||
task.set_null_candidates()
|
||||
|
||||
def user_query(config):
|
||||
def user_query():
|
||||
"""A coroutine for interfacing with the user about the tagging
|
||||
process. lib is the Library to import into and logfile may be
|
||||
a file-like object for logging the import process. The coroutine
|
||||
|
|
@ -580,7 +583,7 @@ def user_query(config):
|
|||
choice = config.choose_match_func(task, config)
|
||||
task.set_choice(choice)
|
||||
log_choice(config, task)
|
||||
plugins.send('import_task_choice', task=task, config=config)
|
||||
plugins.send('import_task_choice', task=task)
|
||||
|
||||
# As-tracks: transition to singleton workflow.
|
||||
if choice is action.TRACKS:
|
||||
|
|
@ -594,8 +597,8 @@ def user_query(config):
|
|||
while True:
|
||||
item_task = yield
|
||||
item_tasks.append(item_task)
|
||||
ipl = pipeline.Pipeline((emitter(), item_lookup(config),
|
||||
item_query(config), collector()))
|
||||
ipl = pipeline.Pipeline((emitter(), item_lookup(),
|
||||
item_query(), collector()))
|
||||
ipl.run_sequential()
|
||||
task = pipeline.multiple(item_tasks)
|
||||
continue
|
||||
|
|
@ -607,11 +610,11 @@ def user_query(config):
|
|||
# imported albums -- those that haven't reached the database
|
||||
# yet.
|
||||
if ident in recent or _duplicate_check(config.lib, task):
|
||||
config.resolve_duplicate_func(task, config)
|
||||
config.resolve_duplicate_func(task)
|
||||
log_choice(config, task, True)
|
||||
recent.add(ident)
|
||||
|
||||
def show_progress(config):
|
||||
def show_progress():
|
||||
"""This stage replaces the initial_lookup and user_query stages
|
||||
when the importer is run without autotagging. It displays the album
|
||||
name and artist as the files are added.
|
||||
|
|
@ -628,7 +631,7 @@ def show_progress(config):
|
|||
task.set_null_candidates()
|
||||
task.set_choice(action.ASIS)
|
||||
|
||||
def apply_choices(config):
|
||||
def apply_choices():
|
||||
"""A coroutine for applying changes to albums and singletons during
|
||||
the autotag process.
|
||||
"""
|
||||
|
|
@ -648,12 +651,11 @@ def apply_choices(config):
|
|||
if task.should_write_tags():
|
||||
if task.is_album:
|
||||
autotag.apply_metadata(
|
||||
task.match.info, task.match.mapping,
|
||||
per_disc_numbering=config.per_disc_numbering
|
||||
task.match.info, task.match.mapping
|
||||
)
|
||||
else:
|
||||
autotag.apply_item_metadata(task.item, task.match.info)
|
||||
plugins.send('import_task_apply', config=config, task=task)
|
||||
plugins.send('import_task_apply', task=task)
|
||||
|
||||
# Infer album-level fields.
|
||||
if task.is_album:
|
||||
|
|
@ -715,7 +717,7 @@ def apply_choices(config):
|
|||
for item in items:
|
||||
config.lib.add(item)
|
||||
|
||||
def plugin_stage(config, func):
|
||||
def plugin_stage(func):
|
||||
"""A coroutine (pipeline stage) that calls the given function with
|
||||
each non-skipped import task. These stages occur between applying
|
||||
metadata changes and moving/copying/writing files.
|
||||
|
|
@ -725,9 +727,9 @@ def plugin_stage(config, func):
|
|||
task = yield task
|
||||
if task.should_skip():
|
||||
continue
|
||||
func(config, task)
|
||||
func(task)
|
||||
|
||||
def manipulate_files(config):
|
||||
def manipulate_files():
|
||||
"""A coroutine (pipeline stage) that performs necessary file
|
||||
manipulations *after* items have been added to the library.
|
||||
"""
|
||||
|
|
@ -741,12 +743,12 @@ def manipulate_files(config):
|
|||
items = task.imported_items()
|
||||
task.old_paths = [item.path for item in items] # For deletion.
|
||||
for item in items:
|
||||
if config.move:
|
||||
if config['import']['move']:
|
||||
# Just move the file.
|
||||
old_path = item.path
|
||||
config.lib.move(item, False)
|
||||
task.prune(old_path)
|
||||
elif config.copy:
|
||||
elif config['import']['copy']:
|
||||
# If it's a reimport, move in-library files and copy
|
||||
# out-of-library files. Otherwise, copy and keep track
|
||||
# of the old path.
|
||||
|
|
@ -766,7 +768,7 @@ def manipulate_files(config):
|
|||
# old paths.
|
||||
config.lib.move(item, True)
|
||||
|
||||
if config.write and task.should_write_tags():
|
||||
if config['import']['write '] and task.should_write_tags():
|
||||
item.write()
|
||||
|
||||
# Save new paths.
|
||||
|
|
@ -775,9 +777,9 @@ def manipulate_files(config):
|
|||
config.lib.store(item)
|
||||
|
||||
# Plugin event.
|
||||
plugins.send('import_task_files', config=config, task=task)
|
||||
plugins.send('import_task_files', task=task)
|
||||
|
||||
def finalize(config):
|
||||
def finalize():
|
||||
"""A coroutine that finishes up importer tasks. In particular, the
|
||||
coroutine sends plugin events, deletes old files, and saves
|
||||
progress. This is a "terminal" coroutine (it yields None).
|
||||
|
|
@ -785,9 +787,9 @@ def finalize(config):
|
|||
while True:
|
||||
task = yield
|
||||
if task.should_skip():
|
||||
if config.resume is not False:
|
||||
if config['import']['resume ']is not False:
|
||||
task.save_progress()
|
||||
if config.incremental:
|
||||
if config['import']['incremental']:
|
||||
task.save_history()
|
||||
continue
|
||||
|
||||
|
|
@ -797,14 +799,14 @@ def finalize(config):
|
|||
if task.is_album:
|
||||
album = config.lib.get_album(task.album_id)
|
||||
plugins.send('album_imported',
|
||||
lib=config.lib, album=album, config=config)
|
||||
lib=config.lib, album=album)
|
||||
else:
|
||||
for item in items:
|
||||
plugins.send('item_imported',
|
||||
lib=config.lib, item=item, config=config)
|
||||
lib=config.lib, item=item)
|
||||
|
||||
# Finally, delete old files.
|
||||
if config.copy and config.delete:
|
||||
if config['import']['copy'] and config['import']['delete']:
|
||||
new_paths = [os.path.realpath(item.path) for item in items]
|
||||
for old_path in task.old_paths:
|
||||
# Only delete files that were actually copied.
|
||||
|
|
@ -813,15 +815,15 @@ def finalize(config):
|
|||
task.prune(old_path)
|
||||
|
||||
# Update progress.
|
||||
if config.resume is not False:
|
||||
if config['import']['resume'] is not False:
|
||||
task.save_progress()
|
||||
if config.incremental:
|
||||
if config['import']['incremental']:
|
||||
task.save_history()
|
||||
|
||||
|
||||
# Singleton pipeline stages.
|
||||
|
||||
def item_lookup(config):
|
||||
def item_lookup():
|
||||
"""A coroutine used to perform the initial MusicBrainz lookup for
|
||||
an item task.
|
||||
"""
|
||||
|
|
@ -831,11 +833,11 @@ def item_lookup(config):
|
|||
if task.sentinel:
|
||||
continue
|
||||
|
||||
plugins.send('import_task_start', task=task, config=config)
|
||||
plugins.send('import_task_start', task=task)
|
||||
|
||||
task.set_item_candidates(*autotag.tag_item(task.item, config.timid))
|
||||
task.set_item_candidates(*autotag.tag_item(task.item))
|
||||
|
||||
def item_query(config):
|
||||
def item_query():
|
||||
"""A coroutine that queries the user for input on single-item
|
||||
lookups.
|
||||
"""
|
||||
|
|
@ -846,20 +848,20 @@ def item_query(config):
|
|||
if task.sentinel:
|
||||
continue
|
||||
|
||||
choice = config.choose_item_func(task, config)
|
||||
choice = config.choose_item_func(task)
|
||||
task.set_choice(choice)
|
||||
log_choice(config, task)
|
||||
plugins.send('import_task_choice', task=task, config=config)
|
||||
plugins.send('import_task_choice', task=task)
|
||||
|
||||
# Duplicate check.
|
||||
if task.choice_flag in (action.ASIS, action.APPLY):
|
||||
ident = task.chosen_ident()
|
||||
if ident in recent or _item_duplicate_check(config.lib, task):
|
||||
config.resolve_duplicate_func(task, config)
|
||||
config.resolve_duplicate_func(task)
|
||||
log_choice(config, task, True)
|
||||
recent.add(ident)
|
||||
|
||||
def item_progress(config):
|
||||
def item_progress():
|
||||
"""Skips the lookup and query stages in a non-autotagged singleton
|
||||
import. Just shows progress.
|
||||
"""
|
||||
|
|
@ -877,41 +879,38 @@ def item_progress(config):
|
|||
|
||||
# Main driver.
|
||||
|
||||
def run_import(**kwargs):
|
||||
"""Run an import. The keyword arguments are the same as those to
|
||||
ImportConfig.
|
||||
def run_import():
|
||||
"""Run an import task.
|
||||
"""
|
||||
config = ImportConfig(**kwargs)
|
||||
|
||||
# Set up the pipeline.
|
||||
if config.query is None:
|
||||
stages = [read_tasks(config)]
|
||||
stages = [read_tasks()]
|
||||
else:
|
||||
stages = [query_tasks(config)]
|
||||
if config.singletons:
|
||||
stages = [query_tasks()]
|
||||
if config['import']['singletons']:
|
||||
# Singleton importer.
|
||||
if config.autot:
|
||||
stages += [item_lookup(config), item_query(config)]
|
||||
if config['import']['autotag']:
|
||||
stages += [item_lookup(), item_query()]
|
||||
else:
|
||||
stages += [item_progress(config)]
|
||||
stages += [item_progress()]
|
||||
else:
|
||||
# Whole-album importer.
|
||||
if config.autot:
|
||||
if config['import']['autotag']:
|
||||
# Only look up and query the user when autotagging.
|
||||
stages += [initial_lookup(config), user_query(config)]
|
||||
stages += [initial_lookup(), user_query()]
|
||||
else:
|
||||
# When not autotagging, just display progress.
|
||||
stages += [show_progress(config)]
|
||||
stages += [apply_choices(config)]
|
||||
stages += [show_progress()]
|
||||
stages += [apply_choices()]
|
||||
for stage_func in plugins.import_stages():
|
||||
stages.append(plugin_stage(config, stage_func))
|
||||
stages += [manipulate_files(config)]
|
||||
stages += [finalize(config)]
|
||||
stages.append(plugin_stage(stage_func))
|
||||
stages += [manipulate_files()]
|
||||
stages += [finalize()]
|
||||
pl = pipeline.Pipeline(stages)
|
||||
|
||||
# Run the pipeline.
|
||||
try:
|
||||
if config.threaded:
|
||||
if config['threaded']:
|
||||
pl.run_parallel(QUEUE_SIZE)
|
||||
else:
|
||||
pl.run_sequential()
|
||||
|
|
|
|||
|
|
@ -48,27 +48,10 @@ if sys.platform == 'win32':
|
|||
|
||||
|
||||
# Constants.
|
||||
CONFIG_PATH_VAR = 'BEETSCONFIG'
|
||||
DEFAULT_CONFIG_FILENAME_UNIX = '.beetsconfig'
|
||||
DEFAULT_CONFIG_FILENAME_WINDOWS = 'beetsconfig.ini'
|
||||
DEFAULT_LIBRARY_FILENAME_UNIX = '.beetsmusic.blb'
|
||||
DEFAULT_LIBRARY_FILENAME_WINDOWS = 'beetsmusic.blb'
|
||||
DEFAULT_DIRECTORY_NAME = 'Music'
|
||||
WINDOWS_BASEDIR = os.environ.get('APPDATA') or '~'
|
||||
PF_KEY_QUERIES = {
|
||||
'comp': 'comp:true',
|
||||
'singleton': 'singleton:true',
|
||||
}
|
||||
DEFAULT_PATH_FORMATS = [
|
||||
(library.PF_KEY_DEFAULT,
|
||||
Template('$albumartist/$album%aunique{}/$track $title')),
|
||||
(PF_KEY_QUERIES['singleton'],
|
||||
Template('Non-Album/$artist/$title')),
|
||||
(PF_KEY_QUERIES['comp'],
|
||||
Template('Compilations/$album%aunique{}/$track $title')),
|
||||
]
|
||||
DEFAULT_ART_FILENAME = 'cover'
|
||||
DEFAULT_TIMEOUT = 5.0
|
||||
|
||||
# UI exception. Commands should throw this in order to display
|
||||
# nonrecoverable errors to the user.
|
||||
|
|
@ -134,7 +117,7 @@ def input_(prompt=None):
|
|||
return resp.decode(sys.stdin.encoding, 'ignore')
|
||||
|
||||
def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
numrange=None, default=None, color=False, max_width=72):
|
||||
numrange=None, default=None, max_width=72):
|
||||
"""Prompts a user for input. The sequence of `options` defines the
|
||||
choices the user has. A single-letter shortcut is inferred for each
|
||||
option; the user's choice is returned as that single, lower-case
|
||||
|
|
@ -192,7 +175,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
|||
is_default = False
|
||||
|
||||
# Possibly colorize the letter shortcut.
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
color = 'turquoise' if is_default else 'blue'
|
||||
show_letter = colorize(color, show_letter)
|
||||
|
||||
|
|
|
|||
|
|
@ -100,35 +100,18 @@ default_commands.append(fields_cmd)
|
|||
|
||||
# import: Autotagger and importer.
|
||||
|
||||
DEFAULT_IMPORT_COPY = True
|
||||
DEFAULT_IMPORT_MOVE = False
|
||||
DEFAULT_IMPORT_WRITE = True
|
||||
DEFAULT_IMPORT_DELETE = False
|
||||
DEFAULT_IMPORT_AUTOT = True
|
||||
DEFAULT_IMPORT_TIMID = False
|
||||
DEFAULT_IMPORT_QUIET = False
|
||||
DEFAULT_IMPORT_QUIET_FALLBACK = 'skip'
|
||||
DEFAULT_IMPORT_RESUME = None # "ask"
|
||||
DEFAULT_IMPORT_INCREMENTAL = False
|
||||
DEFAULT_THREADED = True
|
||||
DEFAULT_COLOR = True
|
||||
DEFAULT_IGNORE = [
|
||||
'.*', '*~',
|
||||
]
|
||||
DEFAULT_PER_DISC_NUMBERING = False
|
||||
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
|
||||
PARTIAL_MATCH_MESSAGE = u'(partial match!)'
|
||||
|
||||
# Importer utilities and support.
|
||||
|
||||
def dist_string(dist, color):
|
||||
def dist_string(dist):
|
||||
"""Formats a distance (a float) as a similarity percentage string.
|
||||
The string is colorized if color is True.
|
||||
"""
|
||||
out = '%.1f%%' % ((1 - dist) * 100)
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
if dist <= autotag.STRONG_REC_THRESH:
|
||||
out = ui.colorize('green', out)
|
||||
elif dist <= autotag.MEDIUM_REC_THRESH:
|
||||
|
|
@ -137,8 +120,7 @@ def dist_string(dist, color):
|
|||
out = ui.colorize('red', out)
|
||||
return out
|
||||
|
||||
def show_change(cur_artist, cur_album, match, color=True,
|
||||
per_disc_numbering=False):
|
||||
def show_change(cur_artist, cur_album, match):
|
||||
"""Print out a representation of the changes that will be made if an
|
||||
album's tags are changed according to `match`, which must be an AlbumMatch
|
||||
object.
|
||||
|
|
@ -156,7 +138,7 @@ def show_change(cur_artist, cur_album, match, color=True,
|
|||
warning = PARTIAL_MATCH_MESSAGE
|
||||
else:
|
||||
warning = None
|
||||
if color and warning:
|
||||
if config['color'].get(bool) and warning:
|
||||
warning = ui.colorize('yellow', warning)
|
||||
|
||||
out = album_description
|
||||
|
|
@ -168,7 +150,7 @@ def show_change(cur_artist, cur_album, match, color=True,
|
|||
"""Return a string representing the track index of the given
|
||||
TrackInfo object.
|
||||
"""
|
||||
if per_disc_numbering:
|
||||
if config['per_disc_numbering'].get(bool):
|
||||
if match.info.mediums > 1:
|
||||
return u'{0}-{1}'.format(track_info.medium,
|
||||
track_info.medium_index)
|
||||
|
|
@ -187,7 +169,7 @@ def show_change(cur_artist, cur_album, match, color=True,
|
|||
# Hide artists for VA releases.
|
||||
artist_l, artist_r = u'', u''
|
||||
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
artist_l, artist_r = ui.colordiff(artist_l, artist_r)
|
||||
album_l, album_r = ui.colordiff(album_l, album_r)
|
||||
|
||||
|
|
@ -199,13 +181,13 @@ def show_change(cur_artist, cur_album, match, color=True,
|
|||
message = u"Tagging: %s - %s" % (match.info.artist, match.info.album)
|
||||
if match.extra_items or match.extra_tracks:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
warning = ui.colorize('yellow', PARTIAL_MATCH_MESSAGE)
|
||||
message += u' ' + warning
|
||||
print_(message)
|
||||
|
||||
# Distance/similarity.
|
||||
print_('(Similarity: %s)' % dist_string(match.distance, color))
|
||||
print_('(Similarity: %s)' % dist_string(match.distance))
|
||||
|
||||
# Tracks.
|
||||
pairs = match.mapping.items()
|
||||
|
|
@ -221,12 +203,12 @@ def show_change(cur_artist, cur_album, match, color=True,
|
|||
if item.length and track_info.length:
|
||||
cur_length = ui.human_seconds_short(item.length)
|
||||
new_length = ui.human_seconds_short(track_info.length)
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
cur_length = ui.colorize('red', cur_length)
|
||||
new_length = ui.colorize('red', new_length)
|
||||
|
||||
# Possibly colorize changes.
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
cur_title, new_title = ui.colordiff(cur_title, new_title)
|
||||
cur_track = ui.colorize('red', cur_track)
|
||||
new_track = ui.colorize('red', new_track)
|
||||
|
|
@ -258,16 +240,16 @@ def show_change(cur_artist, cur_album, match, color=True,
|
|||
for track_info in match.extra_tracks:
|
||||
line = u' * Missing track: {0} ({1})'.format(track_info.title,
|
||||
format_index(track_info))
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
line = ui.colorize('yellow', line)
|
||||
print_(line)
|
||||
for item in match.extra_items:
|
||||
line = u' * Unmatched track: {0} ({1})'.format(item.title, item.track)
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
line = ui.colorize('yellow', line)
|
||||
print_(line)
|
||||
|
||||
def show_item_change(item, match, color):
|
||||
def show_item_change(item, match):
|
||||
"""Print out the change that would occur by tagging `item` with the
|
||||
metadata from `match`, a TrackMatch object.
|
||||
"""
|
||||
|
|
@ -275,7 +257,7 @@ def show_item_change(item, match, color):
|
|||
cur_title, new_title = item.title, match.info.title
|
||||
|
||||
if cur_artist != new_artist or cur_title != new_title:
|
||||
if color:
|
||||
if config['color'].get():
|
||||
cur_artist, new_artist = ui.colordiff(cur_artist, new_artist)
|
||||
cur_title, new_title = ui.colordiff(cur_title, new_title)
|
||||
|
||||
|
|
@ -287,7 +269,7 @@ def show_item_change(item, match, color):
|
|||
else:
|
||||
print_("Tagging track: %s - %s" % (cur_artist, cur_title))
|
||||
|
||||
print_('(Similarity: %s)' % dist_string(match.distance, color))
|
||||
print_('(Similarity: %s)' % dist_string(match.distance))
|
||||
|
||||
def should_resume(config, path):
|
||||
return ui.input_yn("Import of the directory:\n%s"
|
||||
|
|
@ -305,9 +287,8 @@ def _quiet_fall_back(config):
|
|||
assert(False)
|
||||
return config.quiet_fallback
|
||||
|
||||
def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
cur_artist=None, cur_album=None, item=None,
|
||||
itemcount=None, per_disc_numbering=False):
|
||||
def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
||||
cur_album=None, item=None, itemcount=None):
|
||||
"""Given a sorted list of candidates, ask the user for a selection
|
||||
of which candidate to use. Applies to both full albums and
|
||||
singletons (tracks). Candidates are either AlbumMatch or TrackMatch
|
||||
|
|
@ -338,7 +319,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
'https://github.com/sampsyo/beets/wiki/FAQ#wiki-nomatch')
|
||||
opts = ('Use as-is', 'as Tracks', 'Skip', 'Enter search',
|
||||
'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, color=color)
|
||||
sel = ui.input_options(opts)
|
||||
if sel == 'u':
|
||||
return importer.action.ASIS
|
||||
elif sel == 't':
|
||||
|
|
@ -372,7 +353,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
for i, match in enumerate(candidates):
|
||||
print_('%i. %s - %s (%s)' %
|
||||
(i + 1, match.info.artist, match.info.title,
|
||||
dist_string(match.distance, color)))
|
||||
dist_string(match.distance)))
|
||||
else:
|
||||
print_('Finding tags for album "%s - %s".' %
|
||||
(cur_artist, cur_album))
|
||||
|
|
@ -394,12 +375,12 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
elif year:
|
||||
line += u' [%s]' % year
|
||||
|
||||
line += ' (%s)' % dist_string(match.distance, color)
|
||||
line += ' (%s)' % dist_string(match.distance)
|
||||
|
||||
# Point out the partial matches.
|
||||
if match.extra_items or match.extra_tracks:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
if config['color'].get(bool):
|
||||
warning = ui.colorize('yellow', warning)
|
||||
line += u' %s' % warning
|
||||
|
||||
|
|
@ -412,8 +393,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
else:
|
||||
opts = ('Skip', 'Use as-is', 'as Tracks', 'Enter search',
|
||||
'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, numrange=(1, len(candidates)),
|
||||
color=color)
|
||||
sel = ui.input_options(opts, numrange=(1, len(candidates)))
|
||||
if sel == 's':
|
||||
return importer.action.SKIP
|
||||
elif sel == 'u':
|
||||
|
|
@ -436,13 +416,12 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
|
||||
# Show what we're about to do.
|
||||
if singleton:
|
||||
show_item_change(item, match, color)
|
||||
show_item_change(item, match)
|
||||
else:
|
||||
show_change(cur_artist, cur_album, match, color,
|
||||
per_disc_numbering)
|
||||
show_change(cur_artist, cur_album, match)
|
||||
|
||||
# Exact match => tag automatically if we're not in timid mode.
|
||||
if rec == autotag.RECOMMEND_STRONG and not timid:
|
||||
if rec == autotag.RECOMMEND_STRONG and not config['import']['timid']:
|
||||
return match
|
||||
|
||||
# Ask for confirmation.
|
||||
|
|
@ -452,7 +431,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
else:
|
||||
opts = ('Apply', 'More candidates', 'Skip', 'Use as-is',
|
||||
'as Tracks', 'Enter search', 'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, color=color)
|
||||
sel = ui.input_options(opts)
|
||||
if sel == 'a':
|
||||
return match
|
||||
elif sel == 'm':
|
||||
|
|
@ -508,7 +487,7 @@ def choose_match(task, config):
|
|||
# No input; just make a decision.
|
||||
if task.rec == autotag.RECOMMEND_STRONG:
|
||||
match = task.candidates[0]
|
||||
show_change(task.cur_artist, task.cur_album, match, config.color)
|
||||
show_change(task.cur_artist, task.cur_album, match)
|
||||
return match
|
||||
else:
|
||||
return _quiet_fall_back(config)
|
||||
|
|
@ -517,10 +496,8 @@ def choose_match(task, config):
|
|||
candidates, rec = task.candidates, task.rec
|
||||
while True:
|
||||
# Ask for a choice from the user.
|
||||
choice = choose_candidate(candidates, False, rec, config.color,
|
||||
config.timid, task.cur_artist,
|
||||
task.cur_album, itemcount=len(task.items),
|
||||
per_disc_numbering=config.per_disc_numbering)
|
||||
choice = choose_candidate(candidates, False, rec, task.cur_artist,
|
||||
task.cur_album, itemcount=len(task.items))
|
||||
|
||||
# Choose which tags to use.
|
||||
if choice in (importer.action.SKIP, importer.action.ASIS,
|
||||
|
|
@ -532,8 +509,7 @@ def choose_match(task, config):
|
|||
search_artist, search_album = manual_search(False)
|
||||
try:
|
||||
_, _, candidates, rec = \
|
||||
autotag.tag_album(task.items, config.timid, search_artist,
|
||||
search_album)
|
||||
autotag.tag_album(task.items, search_artist, search_album)
|
||||
except autotag.AutotagError:
|
||||
candidates, rec = None, None
|
||||
elif choice is importer.action.MANUAL_ID:
|
||||
|
|
@ -542,8 +518,7 @@ def choose_match(task, config):
|
|||
if search_id:
|
||||
try:
|
||||
_, _, candidates, rec = \
|
||||
autotag.tag_album(task.items, config.timid,
|
||||
search_id=search_id)
|
||||
autotag.tag_album(task.items, search_id=search_id)
|
||||
except autotag.AutotagError:
|
||||
candidates, rec = None, None
|
||||
else:
|
||||
|
|
@ -564,15 +539,14 @@ def choose_item(task, config):
|
|||
# Quiet mode; make a decision.
|
||||
if rec == autotag.RECOMMEND_STRONG:
|
||||
match = candidates[0]
|
||||
show_item_change(task.item, match, config.color)
|
||||
show_item_change(task.item, match)
|
||||
return match
|
||||
else:
|
||||
return _quiet_fall_back(config)
|
||||
|
||||
while True:
|
||||
# Ask for a choice.
|
||||
choice = choose_candidate(candidates, True, rec, config.color,
|
||||
config.timid, item=task.item)
|
||||
choice = choose_candidate(candidates, True, rec, item=task.item)
|
||||
|
||||
if choice in (importer.action.SKIP, importer.action.ASIS):
|
||||
return choice
|
||||
|
|
@ -581,13 +555,13 @@ def choose_item(task, config):
|
|||
elif choice == importer.action.MANUAL:
|
||||
# Continue in the loop with a new set of candidates.
|
||||
search_artist, search_title = manual_search(True)
|
||||
candidates, rec = autotag.tag_item(task.item, config.timid,
|
||||
search_artist, search_title)
|
||||
candidates, rec = autotag.tag_item(task.item, search_artist,
|
||||
search_title)
|
||||
elif choice == importer.action.MANUAL_ID:
|
||||
# Ask for a track ID.
|
||||
search_id = manual_id(True)
|
||||
if search_id:
|
||||
candidates, rec = autotag.tag_item(task.item, config.timid,
|
||||
candidates, rec = autotag.tag_item(task.item,
|
||||
search_id=search_id)
|
||||
else:
|
||||
# Chose a candidate.
|
||||
|
|
@ -607,8 +581,7 @@ def resolve_duplicate(task, config):
|
|||
sel = 's'
|
||||
else:
|
||||
sel = ui.input_options(
|
||||
('Skip new', 'Keep both', 'Remove old'),
|
||||
color=config.color
|
||||
('Skip new', 'Keep both', 'Remove old')
|
||||
)
|
||||
|
||||
if sel == 's':
|
||||
|
|
@ -1139,7 +1112,7 @@ def modify_func(lib, config, opts, args):
|
|||
if not mods:
|
||||
raise ui.UserError('no modifications specified')
|
||||
write = opts.write if opts.write is not None else \
|
||||
config['import_write'].get(bool)
|
||||
config['import']['write'].get(bool)
|
||||
modify_items(lib, mods, query, write, opts.move, opts.album, not opts.yes)
|
||||
modify_cmd.func = modify_func
|
||||
default_commands.append(modify_cmd)
|
||||
|
|
|
|||
Loading…
Reference in a new issue