mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-19 23:44:01 +01:00
InRough update of the beets lib to 1.0b15
This commit is contained in:
+93
-52
@@ -1,5 +1,5 @@
|
||||
# This file is part of beets.
|
||||
# Copyright 2011, Adrian Sampson.
|
||||
# Copyright 2012, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -8,7 +8,7 @@
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
interface. To invoke the CLI, just call beets.ui.main(). The actual
|
||||
CLI commands are implemented in the ui.commands module.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import locale
|
||||
import optparse
|
||||
@@ -27,14 +29,22 @@ import logging
|
||||
import sqlite3
|
||||
import errno
|
||||
import re
|
||||
import codecs
|
||||
|
||||
from lib.beets import library
|
||||
from lib.beets import plugins
|
||||
from lib.beets import util
|
||||
from lib.beets.util.functemplate import Template
|
||||
|
||||
# On Windows platforms, use colorama to support "ANSI" terminal colors.
|
||||
if sys.platform == 'win32':
|
||||
import colorama
|
||||
colorama.init()
|
||||
try:
|
||||
import colorama
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
colorama.init()
|
||||
|
||||
|
||||
# Constants.
|
||||
CONFIG_PATH_VAR = 'BEETSCONFIG'
|
||||
@@ -49,9 +59,12 @@ PF_KEY_QUERIES = {
|
||||
'singleton': 'singleton:true',
|
||||
}
|
||||
DEFAULT_PATH_FORMATS = [
|
||||
(library.PF_KEY_DEFAULT, '$albumartist/$album/$track $title'),
|
||||
(PF_KEY_QUERIES['singleton'], 'Non-Album/$artist/$title'),
|
||||
(PF_KEY_QUERIES['comp'], 'Compilations/$album/$track $title'),
|
||||
(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
|
||||
@@ -94,7 +107,28 @@ def print_(*strings):
|
||||
txt = u''
|
||||
if isinstance(txt, unicode):
|
||||
txt = txt.encode(_encoding(), 'replace')
|
||||
print txt
|
||||
print(txt)
|
||||
|
||||
def input_(prompt=None):
|
||||
"""Like `raw_input`, but decodes the result to a Unicode string.
|
||||
Raises a UserError if stdin is not available. The prompt is sent to
|
||||
stdout rather than stderr. A printed between the prompt and the
|
||||
input cursor.
|
||||
"""
|
||||
# raw_input incorrectly sends prompts to stderr, not stdout, so we
|
||||
# use print() explicitly to display prompts.
|
||||
# http://bugs.python.org/issue1927
|
||||
if prompt:
|
||||
if isinstance(prompt, unicode):
|
||||
prompt = prompt.encode(_encoding(), 'replace')
|
||||
print(prompt, end=' ')
|
||||
|
||||
try:
|
||||
resp = raw_input()
|
||||
except EOFError:
|
||||
raise UserError('stdin stream ended while input required')
|
||||
|
||||
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):
|
||||
@@ -145,7 +179,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
|
||||
# Mark the option's shortcut letter for display.
|
||||
if (default is None and not numrange and first) \
|
||||
or (isinstance(default, basestring) and
|
||||
or (isinstance(default, basestring) and
|
||||
found_letter.lower() == default.lower()):
|
||||
# The first option is the default; mark it.
|
||||
show_letter = '[%s]' % found_letter.upper()
|
||||
@@ -175,7 +209,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
default = numrange[0]
|
||||
else:
|
||||
default = display_letters[0].lower()
|
||||
|
||||
|
||||
# Make a prompt if one is not provided.
|
||||
if not prompt:
|
||||
prompt_parts = []
|
||||
@@ -227,16 +261,14 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
fallback_prompt += '%i-%i, ' % numrange
|
||||
fallback_prompt += ', '.join(display_letters) + ':'
|
||||
|
||||
# (raw_input(prompt) was causing problems with colors.)
|
||||
print prompt,
|
||||
resp = raw_input()
|
||||
resp = input_(prompt)
|
||||
while True:
|
||||
resp = resp.strip().lower()
|
||||
|
||||
|
||||
# Try default option.
|
||||
if default is not None and not resp:
|
||||
resp = default
|
||||
|
||||
|
||||
# Try an integer input if available.
|
||||
if numrange:
|
||||
try:
|
||||
@@ -249,16 +281,15 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
return resp
|
||||
else:
|
||||
resp = None
|
||||
|
||||
|
||||
# Try a normal letter input.
|
||||
if resp:
|
||||
resp = resp[0]
|
||||
if resp in letters:
|
||||
return resp
|
||||
|
||||
|
||||
# Prompt for new input.
|
||||
print fallback_prompt,
|
||||
resp = raw_input()
|
||||
resp = input_(fallback_prompt)
|
||||
|
||||
def input_yn(prompt, require=False, color=False):
|
||||
"""Prompts the user for a "yes" or "no" response. The default is
|
||||
@@ -277,7 +308,7 @@ def config_val(config, section, name, default, vtype=None):
|
||||
"""
|
||||
if not config.has_section(section):
|
||||
config.add_section(section)
|
||||
|
||||
|
||||
try:
|
||||
if vtype is bool:
|
||||
return config.getboolean(section, name)
|
||||
@@ -371,7 +402,7 @@ def colordiff(a, b, highlight='red'):
|
||||
|
||||
a_out = []
|
||||
b_out = []
|
||||
|
||||
|
||||
matcher = SequenceMatcher(lambda x: False, a, b)
|
||||
for op, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
||||
if op == 'equal':
|
||||
@@ -390,7 +421,7 @@ def colordiff(a, b, highlight='red'):
|
||||
b_out.append(colorize(highlight, b[b_start:b_end]))
|
||||
else:
|
||||
assert(False)
|
||||
|
||||
|
||||
return u''.join(a_out), u''.join(b_out)
|
||||
|
||||
def default_paths(pathmod=None):
|
||||
@@ -428,6 +459,8 @@ def _get_replacements(config):
|
||||
repl_string = config_val(config, 'beets', 'replace', None)
|
||||
if not repl_string:
|
||||
return
|
||||
if not isinstance(repl_string, unicode):
|
||||
repl_string = repl_string.decode('utf8')
|
||||
|
||||
parts = repl_string.strip().split()
|
||||
if not parts:
|
||||
@@ -453,10 +486,12 @@ def _get_path_formats(config):
|
||||
legacy_path_format = config_val(config, 'beets', 'path_format', None)
|
||||
if legacy_path_format:
|
||||
# Old path formats override the default values.
|
||||
path_formats = [(library.PF_KEY_DEFAULT, legacy_path_format)]
|
||||
path_formats = [(library.PF_KEY_DEFAULT,
|
||||
Template(legacy_path_format))]
|
||||
else:
|
||||
# If no legacy path format, use the defaults instead.
|
||||
path_formats = DEFAULT_PATH_FORMATS
|
||||
|
||||
if config.has_section('paths'):
|
||||
custom_path_formats = []
|
||||
for key, value in config.items('paths', True):
|
||||
@@ -467,8 +502,9 @@ def _get_path_formats(config):
|
||||
# For non-special keys (literal queries), the _
|
||||
# character denotes a :.
|
||||
key = key.replace('_', ':')
|
||||
custom_path_formats.append((key, value))
|
||||
custom_path_formats.append((key, Template(value)))
|
||||
path_formats = custom_path_formats + path_formats
|
||||
|
||||
return path_formats
|
||||
|
||||
|
||||
@@ -504,7 +540,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
_HelpSubcommand = Subcommand('help', optparse.OptionParser(),
|
||||
help='give detailed help on a specific sub-command',
|
||||
aliases=('?',))
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Create a new subcommand-aware option parser. All of the
|
||||
options to OptionParser.__init__ are supported in addition
|
||||
@@ -513,41 +549,41 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
# The subcommand array, with the help command included.
|
||||
self.subcommands = list(kwargs.pop('subcommands', []))
|
||||
self.subcommands.append(self._HelpSubcommand)
|
||||
|
||||
|
||||
# A more helpful default usage.
|
||||
if 'usage' not in kwargs:
|
||||
kwargs['usage'] = """
|
||||
%prog COMMAND [ARGS...]
|
||||
%prog help COMMAND"""
|
||||
|
||||
|
||||
# Super constructor.
|
||||
optparse.OptionParser.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
# Adjust the help-visible name of each subcommand.
|
||||
for subcommand in self.subcommands:
|
||||
subcommand.parser.prog = '%s %s' % \
|
||||
(self.get_prog_name(), subcommand.name)
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
self.disable_interspersed_args()
|
||||
|
||||
|
||||
def add_subcommand(self, cmd):
|
||||
"""Adds a Subcommand object to the parser's list of commands.
|
||||
"""
|
||||
self.subcommands.append(cmd)
|
||||
|
||||
|
||||
# Add the list of subcommands to the help message.
|
||||
def format_help(self, formatter=None):
|
||||
# Get the original help message, to which we will append.
|
||||
out = optparse.OptionParser.format_help(self, formatter)
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
|
||||
|
||||
# Subcommands header.
|
||||
result = ["\n"]
|
||||
result.append(formatter.format_heading('Commands'))
|
||||
formatter.indent()
|
||||
|
||||
|
||||
# Generate the display names (including aliases).
|
||||
# Also determine the help position.
|
||||
disp_names = []
|
||||
@@ -557,12 +593,12 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
if subcommand.aliases:
|
||||
name += ' (%s)' % ', '.join(subcommand.aliases)
|
||||
disp_names.append(name)
|
||||
|
||||
|
||||
# Set the help position based on the max width.
|
||||
proposed_help_position = len(name) + formatter.current_indent + 2
|
||||
if proposed_help_position <= formatter.max_help_position:
|
||||
help_position = max(help_position, proposed_help_position)
|
||||
|
||||
help_position = max(help_position, proposed_help_position)
|
||||
|
||||
# Add each subcommand to the output.
|
||||
for subcommand, name in zip(self.subcommands, disp_names):
|
||||
# Lifted directly from optparse.py.
|
||||
@@ -581,11 +617,11 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
result.extend(["%*s%s\n" % (help_position, "", line)
|
||||
for line in help_lines[1:]])
|
||||
formatter.dedent()
|
||||
|
||||
|
||||
# Concatenate the original help message with the subcommand
|
||||
# list.
|
||||
return out + "".join(result)
|
||||
|
||||
|
||||
def _subcommand_for_name(self, name):
|
||||
"""Return the subcommand in self.subcommands matching the
|
||||
given name. The name may either be the name of a subcommand or
|
||||
@@ -596,16 +632,16 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
name in subcommand.aliases:
|
||||
return subcommand
|
||||
return None
|
||||
|
||||
|
||||
def parse_args(self, a=None, v=None):
|
||||
"""Like OptionParser.parse_args, but returns these four items:
|
||||
- options: the options passed to the root parser
|
||||
- subcommand: the Subcommand object that was invoked
|
||||
- suboptions: the options passed to the subcommand parser
|
||||
- subargs: the positional arguments passed to the subcommand
|
||||
"""
|
||||
"""
|
||||
options, args = optparse.OptionParser.parse_args(self, a, v)
|
||||
|
||||
|
||||
if not args:
|
||||
# No command given.
|
||||
self.print_help()
|
||||
@@ -615,7 +651,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
subcommand = self._subcommand_for_name(cmdname)
|
||||
if not subcommand:
|
||||
self.error('unknown command ' + cmdname)
|
||||
|
||||
|
||||
suboptions, subargs = subcommand.parser.parse_args(args)
|
||||
|
||||
if subcommand is self._HelpSubcommand:
|
||||
@@ -623,13 +659,15 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
# particular
|
||||
cmdname = subargs[0]
|
||||
helpcommand = self._subcommand_for_name(cmdname)
|
||||
if not helpcommand:
|
||||
self.error('no command named {0}'.format(cmdname))
|
||||
helpcommand.parser.print_help()
|
||||
self.exit()
|
||||
else:
|
||||
# general
|
||||
self.print_help()
|
||||
self.exit()
|
||||
|
||||
|
||||
return options, subcommand, suboptions, subargs
|
||||
|
||||
|
||||
@@ -654,7 +692,7 @@ def main(args=None, configfh=None):
|
||||
if configpath:
|
||||
configpath = util.syspath(configpath)
|
||||
if os.path.exists(util.syspath(configpath)):
|
||||
configfh = open(configpath)
|
||||
configfh = codecs.open(configpath, 'r', encoding='utf-8')
|
||||
else:
|
||||
configfh = None
|
||||
if configfh:
|
||||
@@ -667,7 +705,6 @@ def main(args=None, configfh=None):
|
||||
# Load requested plugins.
|
||||
plugnames = config_val(config, 'beets', 'plugins', '')
|
||||
plugins.load_plugins(plugnames.split())
|
||||
plugins.load_listeners()
|
||||
plugins.send("pluginload")
|
||||
plugins.configure(config)
|
||||
|
||||
@@ -681,10 +718,10 @@ def main(args=None, configfh=None):
|
||||
help="destination music directory")
|
||||
parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
|
||||
help='print debugging information')
|
||||
|
||||
|
||||
# Parse the command-line!
|
||||
options, subcommand, suboptions, subargs = parser.parse_args(args)
|
||||
|
||||
|
||||
# Open library file.
|
||||
libpath = options.libpath or \
|
||||
config_val(config, 'beets', 'library', default_libpath)
|
||||
@@ -709,7 +746,8 @@ def main(args=None, configfh=None):
|
||||
replacements)
|
||||
except sqlite3.OperationalError:
|
||||
raise UserError("database file %s could not be opened" % db_path)
|
||||
|
||||
plugins.send("library_opened", lib=lib)
|
||||
|
||||
# Configure the logger.
|
||||
log = logging.getLogger('beets')
|
||||
if options.verbose:
|
||||
@@ -719,14 +757,17 @@ def main(args=None, configfh=None):
|
||||
log.debug(u'config file: %s' % util.displayable_path(configpath))
|
||||
log.debug(u'library database: %s' % util.displayable_path(lib.path))
|
||||
log.debug(u'library directory: %s' % util.displayable_path(lib.directory))
|
||||
|
||||
|
||||
# Invoke the subcommand.
|
||||
try:
|
||||
subcommand.func(lib, config, suboptions, subargs)
|
||||
except UserError, exc:
|
||||
except UserError as exc:
|
||||
message = exc.args[0] if exc.args else None
|
||||
subcommand.parser.error(message)
|
||||
except IOError, exc:
|
||||
except util.HumanReadableException as exc:
|
||||
exc.log(log)
|
||||
sys.exit(1)
|
||||
except IOError as exc:
|
||||
if exc.errno == errno.EPIPE:
|
||||
# "Broken pipe". End silently.
|
||||
pass
|
||||
|
||||
+287
-247
@@ -1,5 +1,5 @@
|
||||
# This file is part of beets.
|
||||
# Copyright 2011, Adrian Sampson.
|
||||
# Copyright 2012, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -8,25 +8,25 @@
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""This module provides the default commands for beets' command-line
|
||||
interface.
|
||||
"""
|
||||
from __future__ import with_statement # Python 2.5
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import itertools
|
||||
import re
|
||||
|
||||
import lib.beets
|
||||
from lib.beets import ui
|
||||
from lib.beets.ui import print_, decargs
|
||||
from lib.beets.ui import print_, input_, decargs
|
||||
from lib.beets import autotag
|
||||
import lib.beets.autotag.art
|
||||
from lib.beets import plugins
|
||||
from lib.beets import importer
|
||||
from lib.beets.util import syspath, normpath, ancestry, displayable_path
|
||||
@@ -64,7 +64,7 @@ def _do_query(lib, query, album, also_items=True):
|
||||
raise ui.UserError('No matching albums found.')
|
||||
elif not album and not items:
|
||||
raise ui.UserError('No matching items found.')
|
||||
|
||||
|
||||
return items, albums
|
||||
|
||||
FLOAT_EPSILON = 0.01
|
||||
@@ -84,14 +84,27 @@ def _showdiff(field, oldval, newval, color):
|
||||
print_(u' %s: %s -> %s' % (field, oldval, newval))
|
||||
|
||||
|
||||
# fields: Shows a list of available fields for queries and format strings.
|
||||
fields_cmd = ui.Subcommand('fields',
|
||||
help='show fields available for queries and format strings')
|
||||
def fields_func(lib, config, opts, args):
|
||||
print("Available item fields:")
|
||||
print(" " + "\n ".join([key for key in library.ITEM_KEYS]))
|
||||
print("\nAvailable album fields:")
|
||||
print(" " + "\n ".join([key for key in library.ALBUM_KEYS]))
|
||||
|
||||
fields_cmd.func = fields_func
|
||||
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_ART = True
|
||||
DEFAULT_IMPORT_QUIET = False
|
||||
DEFAULT_IMPORT_QUIET_FALLBACK = 'skip'
|
||||
DEFAULT_IMPORT_RESUME = None # "ask"
|
||||
@@ -101,6 +114,7 @@ DEFAULT_COLOR = True
|
||||
DEFAULT_IGNORE = [
|
||||
'.*', '*~',
|
||||
]
|
||||
DEFAULT_PER_DISC_NUMBERING = False
|
||||
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
|
||||
@@ -122,10 +136,11 @@ def dist_string(dist, color):
|
||||
out = ui.colorize('red', out)
|
||||
return out
|
||||
|
||||
def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
"""Print out a representation of the changes that will be made if
|
||||
tags are changed from (cur_artist, cur_album, items) to info with
|
||||
distance dist.
|
||||
def show_change(cur_artist, cur_album, match, color=True,
|
||||
per_disc_numbering=False):
|
||||
"""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.
|
||||
"""
|
||||
def show_album(artist, album, partial=False):
|
||||
if artist:
|
||||
@@ -148,14 +163,25 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
out += u' ' + warning
|
||||
print_(out)
|
||||
|
||||
# Record if the match is partial or not.
|
||||
partial_match = None in items
|
||||
def format_index(track_info):
|
||||
"""Return a string representing the track index of the given
|
||||
TrackInfo object.
|
||||
"""
|
||||
if per_disc_numbering:
|
||||
if match.info.mediums > 1:
|
||||
return u'{0}-{1}'.format(track_info.medium,
|
||||
track_info.medium_index)
|
||||
else:
|
||||
return unicode(track_info.medium_index)
|
||||
else:
|
||||
return unicode(track_info.index)
|
||||
|
||||
# Identify the album in question.
|
||||
if cur_artist != info.artist or \
|
||||
(cur_album != info.album and info.album != VARIOUS_ARTISTS):
|
||||
artist_l, artist_r = cur_artist or '', info.artist
|
||||
album_l, album_r = cur_album or '', info.album
|
||||
if cur_artist != match.info.artist or \
|
||||
(cur_album != match.info.album and
|
||||
match.info.album != VARIOUS_ARTISTS):
|
||||
artist_l, artist_r = cur_artist or '', match.info.artist
|
||||
album_l, album_r = cur_album or '', match.info.album
|
||||
if artist_r == VARIOUS_ARTISTS:
|
||||
# Hide artists for VA releases.
|
||||
artist_l, artist_r = u'', u''
|
||||
@@ -169,8 +195,8 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
print_("To:")
|
||||
show_album(artist_r, album_r)
|
||||
else:
|
||||
message = u"Tagging: %s - %s" % (info.artist, info.album)
|
||||
if partial_match:
|
||||
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:
|
||||
warning = ui.colorize('yellow', PARTIAL_MATCH_MESSAGE)
|
||||
@@ -178,18 +204,17 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
print_(message)
|
||||
|
||||
# Distance/similarity.
|
||||
print_('(Similarity: %s)' % dist_string(dist, color))
|
||||
print_('(Similarity: %s)' % dist_string(match.distance, color))
|
||||
|
||||
# Tracks.
|
||||
missing_tracks = []
|
||||
for i, (item, track_info) in enumerate(zip(items, info.tracks)):
|
||||
if not item:
|
||||
missing_tracks.append((i, track_info))
|
||||
continue
|
||||
|
||||
pairs = match.mapping.items()
|
||||
pairs.sort(key=lambda (_, track_info): track_info.index)
|
||||
for item, track_info in pairs:
|
||||
# Get displayable LHS and RHS values.
|
||||
cur_track = unicode(item.track)
|
||||
new_track = unicode(i+1)
|
||||
new_track = format_index(track_info)
|
||||
tracks_differ = item.track not in (track_info.index,
|
||||
track_info.medium_index)
|
||||
cur_title = item.title
|
||||
new_title = track_info.title
|
||||
if item.length and track_info.length:
|
||||
@@ -198,48 +223,55 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
if color:
|
||||
cur_length = ui.colorize('red', cur_length)
|
||||
new_length = ui.colorize('red', new_length)
|
||||
|
||||
|
||||
# Possibly colorize changes.
|
||||
if color:
|
||||
cur_title, new_title = ui.colordiff(cur_title, new_title)
|
||||
if cur_track != new_track:
|
||||
cur_track = ui.colorize('red', cur_track)
|
||||
new_track = ui.colorize('red', new_track)
|
||||
cur_track = ui.colorize('red', cur_track)
|
||||
new_track = ui.colorize('red', new_track)
|
||||
|
||||
# Show filename (non-colorized) when title is not set.
|
||||
if not item.title.strip():
|
||||
cur_title = displayable_path(os.path.basename(item.path))
|
||||
|
||||
|
||||
if cur_title != new_title:
|
||||
lhs, rhs = cur_title, new_title
|
||||
if cur_track != new_track:
|
||||
if tracks_differ:
|
||||
lhs += u' (%s)' % cur_track
|
||||
rhs += u' (%s)' % new_track
|
||||
print_(u" * %s -> %s" % (lhs, rhs))
|
||||
else:
|
||||
line = u' * %s' % item.title
|
||||
display = False
|
||||
if cur_track != new_track:
|
||||
if tracks_differ:
|
||||
display = True
|
||||
line += u' (%s -> %s)' % (cur_track, new_track)
|
||||
if item.length and track_info.length and \
|
||||
abs(item.length - track_info.length) > 2.0:
|
||||
display = True
|
||||
line += u' (%s -> %s)' % (cur_length, new_length)
|
||||
line += u' (%s vs. %s)' % (cur_length, new_length)
|
||||
if display:
|
||||
print_(line)
|
||||
for i, track_info in missing_tracks:
|
||||
line = u' * Missing track: %s (%d)' % (track_info.title, i+1)
|
||||
|
||||
# Missing and unmatched tracks.
|
||||
for track_info in match.extra_tracks:
|
||||
line = u' * Missing track: {0} ({1})'.format(track_info.title,
|
||||
format_index(track_info))
|
||||
if color:
|
||||
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:
|
||||
line = ui.colorize('yellow', line)
|
||||
print_(line)
|
||||
|
||||
def show_item_change(item, info, dist, color):
|
||||
def show_item_change(item, match, color):
|
||||
"""Print out the change that would occur by tagging `item` with the
|
||||
metadata from `info`.
|
||||
metadata from `match`, a TrackMatch object.
|
||||
"""
|
||||
cur_artist, new_artist = item.artist, info.artist
|
||||
cur_title, new_title = item.title, info.title
|
||||
cur_artist, new_artist = item.artist, match.info.artist
|
||||
cur_title, new_title = item.title, match.info.title
|
||||
|
||||
if cur_artist != new_artist or cur_title != new_title:
|
||||
if color:
|
||||
@@ -254,7 +286,7 @@ def show_item_change(item, info, dist, color):
|
||||
else:
|
||||
print_("Tagging track: %s - %s" % (cur_artist, cur_title))
|
||||
|
||||
print_('(Similarity: %s)' % dist_string(dist, color))
|
||||
print_('(Similarity: %s)' % dist_string(match.distance, color))
|
||||
|
||||
def should_resume(config, path):
|
||||
return ui.input_yn("Import of the directory:\n%s"
|
||||
@@ -273,17 +305,17 @@ def _quiet_fall_back(config):
|
||||
return config.quiet_fallback
|
||||
|
||||
def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
cur_artist=None, cur_album=None, item=None):
|
||||
cur_artist=None, cur_album=None, item=None,
|
||||
itemcount=None, per_disc_numbering=False):
|
||||
"""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). For albums, the candidates are `(dist, items,
|
||||
info)` triples and `cur_artist` and `cur_album` must be provided.
|
||||
For singletons, the candidates are `(dist, info)` pairs and `item`
|
||||
must be provided.
|
||||
of which candidate to use. Applies to both full albums and
|
||||
singletons (tracks). Candidates are either AlbumMatch or TrackMatch
|
||||
objects depending on `singleton`. for albums, `cur_artist`,
|
||||
`cur_album`, and `itemcount` must be provided. For singletons,
|
||||
`item` must be provided.
|
||||
|
||||
Returns the result of the choice, which may SKIP, ASIS, TRACKS, or
|
||||
MANUAL or a candidate. For albums, a candidate is a `(info, items)`
|
||||
pair; for items, it is just a TrackInfo object.
|
||||
MANUAL or a candidate (an AlbumMatch/TrackMatch object).
|
||||
"""
|
||||
# Sanity check.
|
||||
if singleton:
|
||||
@@ -294,11 +326,15 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
|
||||
# Zero candidates.
|
||||
if not candidates:
|
||||
print_("No match found.")
|
||||
if singleton:
|
||||
print_("No matching recordings found.")
|
||||
opts = ('Use as-is', 'Skip', 'Enter search', 'enter Id',
|
||||
'aBort')
|
||||
else:
|
||||
print_("No matching release found for {0} tracks."
|
||||
.format(itemcount))
|
||||
print_('For help, see: '
|
||||
'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)
|
||||
@@ -321,12 +357,9 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
# Is the change good enough?
|
||||
bypass_candidates = False
|
||||
if rec != autotag.RECOMMEND_NONE:
|
||||
if singleton:
|
||||
dist, info = candidates[0]
|
||||
else:
|
||||
dist, items, info = candidates[0]
|
||||
match = candidates[0]
|
||||
bypass_candidates = True
|
||||
|
||||
|
||||
while True:
|
||||
# Display and choose from candidates.
|
||||
if not bypass_candidates:
|
||||
@@ -335,22 +368,24 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
print_('Finding tags for track "%s - %s".' %
|
||||
(item.artist, item.title))
|
||||
print_('Candidates:')
|
||||
for i, (dist, info) in enumerate(candidates):
|
||||
print_('%i. %s - %s (%s)' % (i+1, info.artist,
|
||||
info.title, dist_string(dist, color)))
|
||||
for i, match in enumerate(candidates):
|
||||
print_('%i. %s - %s (%s)' %
|
||||
(i + 1, match.info.artist, match.info.title,
|
||||
dist_string(match.distance, color)))
|
||||
else:
|
||||
print_('Finding tags for album "%s - %s".' %
|
||||
(cur_artist, cur_album))
|
||||
print_('Candidates:')
|
||||
for i, (dist, items, info) in enumerate(candidates):
|
||||
line = '%i. %s - %s' % (i+1, info.artist, info.album)
|
||||
for i, match in enumerate(candidates):
|
||||
line = '%i. %s - %s' % (i + 1, match.info.artist,
|
||||
match.info.album)
|
||||
|
||||
# Label and year disambiguation, if available.
|
||||
label, year = None, None
|
||||
if info.label:
|
||||
label = info.label
|
||||
if info.year:
|
||||
year = unicode(info.year)
|
||||
if match.info.label:
|
||||
label = match.info.label
|
||||
if match.info.year:
|
||||
year = unicode(match.info.year)
|
||||
if label and year:
|
||||
line += u' [%s, %s]' % (label, year)
|
||||
elif label:
|
||||
@@ -358,17 +393,17 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
elif year:
|
||||
line += u' [%s]' % year
|
||||
|
||||
line += ' (%s)' % dist_string(dist, color)
|
||||
line += ' (%s)' % dist_string(match.distance, color)
|
||||
|
||||
# Point out the partial matches.
|
||||
if None in items:
|
||||
if match.extra_items or match.extra_tracks:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
warning = ui.colorize('yellow', warning)
|
||||
line += u' %s' % warning
|
||||
|
||||
print_(line)
|
||||
|
||||
|
||||
# Ask the user for a choice.
|
||||
if singleton:
|
||||
opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id',
|
||||
@@ -391,26 +426,24 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
raise importer.ImportAbort()
|
||||
elif sel == 'i':
|
||||
return importer.action.MANUAL_ID
|
||||
else: # Numerical selection.
|
||||
else: # Numerical selection.
|
||||
if singleton:
|
||||
dist, info = candidates[sel-1]
|
||||
match = candidates[sel - 1]
|
||||
else:
|
||||
dist, items, info = candidates[sel-1]
|
||||
match = candidates[sel - 1]
|
||||
bypass_candidates = False
|
||||
|
||||
|
||||
# Show what we're about to do.
|
||||
if singleton:
|
||||
show_item_change(item, info, dist, color)
|
||||
show_item_change(item, match, color)
|
||||
else:
|
||||
show_change(cur_artist, cur_album, items, info, dist, color)
|
||||
|
||||
show_change(cur_artist, cur_album, match, color,
|
||||
per_disc_numbering)
|
||||
|
||||
# Exact match => tag automatically if we're not in timid mode.
|
||||
if rec == autotag.RECOMMEND_STRONG and not timid:
|
||||
if singleton:
|
||||
return info
|
||||
else:
|
||||
return info, items
|
||||
|
||||
return match
|
||||
|
||||
# Ask for confirmation.
|
||||
if singleton:
|
||||
opts = ('Apply', 'More candidates', 'Skip', 'Use as-is',
|
||||
@@ -420,10 +453,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
'as Tracks', 'Enter search', 'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, color=color)
|
||||
if sel == 'a':
|
||||
if singleton:
|
||||
return info
|
||||
else:
|
||||
return info, items
|
||||
return match
|
||||
elif sel == 'm':
|
||||
pass
|
||||
elif sel == 's':
|
||||
@@ -444,18 +474,17 @@ def manual_search(singleton):
|
||||
"""Input either an artist and album (for full albums) or artist and
|
||||
track name (for singletons) for manual search.
|
||||
"""
|
||||
artist = raw_input('Artist: ').decode(sys.stdin.encoding)
|
||||
name = raw_input('Track: ' if singleton else 'Album: ') \
|
||||
.decode(sys.stdin.encoding)
|
||||
artist = input_('Artist:')
|
||||
name = input_('Track:' if singleton else 'Album:')
|
||||
return artist.strip(), name.strip()
|
||||
|
||||
def manual_id(singleton):
|
||||
"""Input a MusicBrainz ID, either for an album ("release") or a
|
||||
track ("recording"). If no valid ID is entered, returns None.
|
||||
"""
|
||||
prompt = 'Enter MusicBrainz %s ID: ' % \
|
||||
prompt = 'Enter MusicBrainz %s ID:' % \
|
||||
('recording' if singleton else 'release')
|
||||
entry = raw_input(prompt).decode(sys.stdin.encoding).strip()
|
||||
entry = input_(prompt).strip()
|
||||
|
||||
# Find the first thing that looks like a UUID/MBID.
|
||||
match = re.search('[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}', entry)
|
||||
@@ -468,7 +497,7 @@ def manual_id(singleton):
|
||||
def choose_match(task, config):
|
||||
"""Given an initial autotagging of items, go through an interactive
|
||||
dance with the user to ask for a choice of metadata. Returns an
|
||||
(info, items) pair, ASIS, or SKIP.
|
||||
AlbumMatch object, ASIS, or SKIP.
|
||||
"""
|
||||
# Show what we're tagging.
|
||||
print_()
|
||||
@@ -477,10 +506,9 @@ def choose_match(task, config):
|
||||
if config.quiet:
|
||||
# No input; just make a decision.
|
||||
if task.rec == autotag.RECOMMEND_STRONG:
|
||||
dist, items, info = task.candidates[0]
|
||||
show_change(task.cur_artist, task.cur_album, items, info, dist,
|
||||
config.color)
|
||||
return info, items
|
||||
match = task.candidates[0]
|
||||
show_change(task.cur_artist, task.cur_album, match, config.color)
|
||||
return match
|
||||
else:
|
||||
return _quiet_fall_back(config)
|
||||
|
||||
@@ -488,10 +516,11 @@ 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,
|
||||
choice = choose_candidate(candidates, False, rec, config.color,
|
||||
config.timid, task.cur_artist,
|
||||
task.cur_album)
|
||||
|
||||
task.cur_album, itemcount=len(task.items),
|
||||
per_disc_numbering=config.per_disc_numbering)
|
||||
|
||||
# Choose which tags to use.
|
||||
if choice in (importer.action.SKIP, importer.action.ASIS,
|
||||
importer.action.TRACKS):
|
||||
@@ -517,25 +546,25 @@ def choose_match(task, config):
|
||||
except autotag.AutotagError:
|
||||
candidates, rec = None, None
|
||||
else:
|
||||
# We have a candidate! Finish tagging. Here, choice is
|
||||
# an (info, items) pair as desired.
|
||||
assert not isinstance(choice, importer.action)
|
||||
# We have a candidate! Finish tagging. Here, choice is an
|
||||
# AlbumMatch object.
|
||||
assert isinstance(choice, autotag.AlbumMatch)
|
||||
return choice
|
||||
|
||||
def choose_item(task, config):
|
||||
"""Ask the user for a choice about tagging a single item. Returns
|
||||
either an action constant or a TrackInfo object.
|
||||
either an action constant or a TrackMatch object.
|
||||
"""
|
||||
print_()
|
||||
print_(task.item.path)
|
||||
candidates, rec = task.item_match
|
||||
candidates, rec = task.candidates, task.rec
|
||||
|
||||
if config.quiet:
|
||||
# Quiet mode; make a decision.
|
||||
if rec == autotag.RECOMMEND_STRONG:
|
||||
dist, track_info = candidates[0]
|
||||
show_item_change(task.item, track_info, dist, config.color)
|
||||
return track_info
|
||||
match = candidates[0]
|
||||
show_item_change(task.item, match, config.color)
|
||||
return match
|
||||
else:
|
||||
return _quiet_fall_back(config)
|
||||
|
||||
@@ -558,10 +587,10 @@ def choose_item(task, config):
|
||||
search_id = manual_id(True)
|
||||
if search_id:
|
||||
candidates, rec = autotag.tag_item(task.item, config.timid,
|
||||
search_id=search_id)
|
||||
search_id=search_id)
|
||||
else:
|
||||
# Chose a candidate.
|
||||
assert not isinstance(choice, importer.action)
|
||||
assert isinstance(choice, autotag.TrackMatch)
|
||||
return choice
|
||||
|
||||
def resolve_duplicate(task, config):
|
||||
@@ -595,30 +624,30 @@ def resolve_duplicate(task, config):
|
||||
|
||||
# The import command.
|
||||
|
||||
def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
def import_files(lib, paths, copy, move, write, autot, logpath, threaded,
|
||||
color, delete, quiet, resume, quiet_fallback, singletons,
|
||||
timid, query, incremental, ignore):
|
||||
timid, query, incremental, ignore, per_disc_numbering):
|
||||
"""Import the files in the given list of paths, tagging each leaf
|
||||
directory as an album. If copy, then the files are copied into
|
||||
the library folder. If write, then new metadata is written to the
|
||||
files themselves. If not autot, then just import the files
|
||||
without attempting to tag. If logpath is provided, then untaggable
|
||||
albums will be logged there. If art, then attempt to download
|
||||
cover art for each album. If threaded, then accelerate autotagging
|
||||
directory as an album. If copy, then the files are copied into the
|
||||
library folder. If write, then new metadata is written to the files
|
||||
themselves. If not autot, then just import the files without
|
||||
attempting to tag. If logpath is provided, then untaggable albums
|
||||
will be logged there. If threaded, then accelerate autotagging
|
||||
imports by running them in multiple threads. If color, then
|
||||
ANSI-colorize some terminal output. If delete, then old files are
|
||||
deleted when they are copied. If quiet, then the user is
|
||||
never prompted for input; instead, the tagger just skips anything
|
||||
it is not confident about. resume indicates whether interrupted
|
||||
imports can be resumed and is either a boolean or None.
|
||||
quiet_fallback should be either ASIS or SKIP and indicates what
|
||||
should happen in quiet mode when the recommendation is not strong.
|
||||
deleted when they are copied. If quiet, then the user is never
|
||||
prompted for input; instead, the tagger just skips anything it is
|
||||
not confident about. resume indicates whether interrupted imports
|
||||
can be resumed and is either a boolean or None. quiet_fallback
|
||||
should be either ASIS or SKIP and indicates what should happen in
|
||||
quiet mode when the recommendation is not strong.
|
||||
"""
|
||||
# Check the user-specified directories.
|
||||
for path in paths:
|
||||
if not singletons and not os.path.isdir(syspath(path)):
|
||||
fullpath = syspath(normpath(path))
|
||||
if not singletons and not os.path.isdir(fullpath):
|
||||
raise ui.UserError('not a directory: ' + path)
|
||||
elif singletons and not os.path.exists(syspath(path)):
|
||||
elif singletons and not os.path.exists(fullpath):
|
||||
raise ui.UserError('no such file: ' + path)
|
||||
|
||||
# Check parameter consistency.
|
||||
@@ -633,7 +662,7 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
except IOError:
|
||||
raise ui.UserError(u"could not open log file for writing: %s" %
|
||||
displayable_path(logpath))
|
||||
print >>logfile, 'import started', time.asctime()
|
||||
print('import started', time.asctime(), file=logfile)
|
||||
else:
|
||||
logfile = None
|
||||
|
||||
@@ -652,8 +681,8 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
quiet = quiet,
|
||||
quiet_fallback = quiet_fallback,
|
||||
copy = copy,
|
||||
move = move,
|
||||
write = write,
|
||||
art = art,
|
||||
delete = delete,
|
||||
threaded = threaded,
|
||||
autot = autot,
|
||||
@@ -666,12 +695,13 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
incremental = incremental,
|
||||
ignore = ignore,
|
||||
resolve_duplicate_func = resolve_duplicate,
|
||||
per_disc_numbering = per_disc_numbering,
|
||||
)
|
||||
|
||||
|
||||
finally:
|
||||
# If we were logging, close the file.
|
||||
if logfile:
|
||||
print >>logfile, ''
|
||||
print('', file=logfile)
|
||||
logfile.close()
|
||||
|
||||
# Emit event.
|
||||
@@ -696,10 +726,6 @@ import_cmd.parser.add_option('-p', '--resume', action='store_true',
|
||||
default=None, help="resume importing if interrupted")
|
||||
import_cmd.parser.add_option('-P', '--noresume', action='store_false',
|
||||
dest='resume', help="do not try to resume importing")
|
||||
import_cmd.parser.add_option('-r', '--art', action='store_true',
|
||||
default=None, help="try to download album art")
|
||||
import_cmd.parser.add_option('-R', '--noart', action='store_false',
|
||||
dest='art', help="don't album art (opposite of -r)")
|
||||
import_cmd.parser.add_option('-q', '--quiet', action='store_true',
|
||||
dest='quiet', help="never prompt for input: skip albums instead")
|
||||
import_cmd.parser.add_option('-l', '--log', dest='logpath',
|
||||
@@ -712,19 +738,20 @@ import_cmd.parser.add_option('-L', '--library', dest='library',
|
||||
action='store_true', help='retag items matching a query')
|
||||
import_cmd.parser.add_option('-i', '--incremental', dest='incremental',
|
||||
action='store_true', help='skip already-imported directories')
|
||||
import_cmd.parser.add_option('-I', '--noincremental', dest='incremental',
|
||||
action='store_false', help='do not skip already-imported directories')
|
||||
def import_func(lib, config, opts, args):
|
||||
copy = opts.copy if opts.copy is not None else \
|
||||
ui.config_val(config, 'beets', 'import_copy',
|
||||
DEFAULT_IMPORT_COPY, bool)
|
||||
move = ui.config_val(config, 'beets', 'import_move',
|
||||
DEFAULT_IMPORT_MOVE, bool)
|
||||
write = opts.write if opts.write is not None else \
|
||||
ui.config_val(config, 'beets', 'import_write',
|
||||
DEFAULT_IMPORT_WRITE, bool)
|
||||
delete = ui.config_val(config, 'beets', 'import_delete',
|
||||
DEFAULT_IMPORT_DELETE, bool)
|
||||
autot = opts.autotag if opts.autotag is not None else DEFAULT_IMPORT_AUTOT
|
||||
art = opts.art if opts.art is not None else \
|
||||
ui.config_val(config, 'beets', 'import_art',
|
||||
DEFAULT_IMPORT_ART, bool)
|
||||
threaded = ui.config_val(config, 'beets', 'threaded',
|
||||
DEFAULT_THREADED, bool)
|
||||
color = ui.config_val(config, 'beets', 'color', DEFAULT_COLOR, bool)
|
||||
@@ -741,6 +768,8 @@ def import_func(lib, config, opts, args):
|
||||
ui.config_val(config, 'beets', 'import_incremental',
|
||||
DEFAULT_IMPORT_INCREMENTAL, bool)
|
||||
ignore = ui.config_val(config, 'beets', 'ignore', DEFAULT_IGNORE, list)
|
||||
per_disc_numbering = ui.config_val(config, 'beets', 'per_disc_numbering',
|
||||
DEFAULT_PER_DISC_NUMBERING, bool)
|
||||
|
||||
# Resume has three options: yes, no, and "ask" (None).
|
||||
resume = opts.resume if opts.resume is not None else \
|
||||
@@ -753,6 +782,11 @@ def import_func(lib, config, opts, args):
|
||||
else:
|
||||
resume = None
|
||||
|
||||
# Special case: --copy flag suppresses import_move (which would
|
||||
# otherwise take precedence).
|
||||
if opts.copy:
|
||||
move = False
|
||||
|
||||
if quiet_fallback_str == 'asis':
|
||||
quiet_fallback = importer.action.ASIS
|
||||
else:
|
||||
@@ -765,26 +799,23 @@ def import_func(lib, config, opts, args):
|
||||
query = None
|
||||
paths = args
|
||||
|
||||
import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
import_files(lib, paths, copy, move, write, autot, logpath, threaded,
|
||||
color, delete, quiet, resume, quiet_fallback, singletons,
|
||||
timid, query, incremental, ignore)
|
||||
timid, query, incremental, ignore, per_disc_numbering)
|
||||
import_cmd.func = import_func
|
||||
default_commands.append(import_cmd)
|
||||
|
||||
|
||||
# list: Query and show library contents.
|
||||
|
||||
DEFAULT_LIST_FORMAT_ITEM = '$artist - $album - $title'
|
||||
DEFAULT_LIST_FORMAT_ALBUM = '$albumartist - $album'
|
||||
|
||||
def list_items(lib, query, album, path, fmt):
|
||||
"""Print out items in lib matching query. If album, then search for
|
||||
albums instead of single items. If path, print the matched objects'
|
||||
paths instead of human-readable information about them.
|
||||
"""
|
||||
if fmt is None:
|
||||
# If no specific template is supplied, use a default.
|
||||
if album:
|
||||
fmt = u'$albumartist - $album'
|
||||
else:
|
||||
fmt = u'$artist - $album - $title'
|
||||
template = Template(fmt)
|
||||
|
||||
if album:
|
||||
@@ -792,13 +823,13 @@ def list_items(lib, query, album, path, fmt):
|
||||
if path:
|
||||
print_(album.item_dir())
|
||||
elif fmt is not None:
|
||||
print_(template.substitute(album._record))
|
||||
print_(album.evaluate_template(template))
|
||||
else:
|
||||
for item in lib.items(query):
|
||||
if path:
|
||||
print_(item.path)
|
||||
elif fmt is not None:
|
||||
print_(template.substitute(item.record))
|
||||
print_(item.evaluate_template(template, lib))
|
||||
|
||||
list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',))
|
||||
list_cmd.parser.add_option('-a', '--album', action='store_true',
|
||||
@@ -808,7 +839,16 @@ list_cmd.parser.add_option('-p', '--path', action='store_true',
|
||||
list_cmd.parser.add_option('-f', '--format', action='store',
|
||||
help='print with custom format', default=None)
|
||||
def list_func(lib, config, opts, args):
|
||||
list_items(lib, decargs(args), opts.album, opts.path, opts.format)
|
||||
fmt = opts.format
|
||||
if not fmt:
|
||||
# If no format is specified, fall back to a default.
|
||||
if opts.album:
|
||||
fmt = ui.config_val(config, 'beets', 'list_format_album',
|
||||
DEFAULT_LIST_FORMAT_ALBUM)
|
||||
else:
|
||||
fmt = ui.config_val(config, 'beets', 'list_format_item',
|
||||
DEFAULT_LIST_FORMAT_ITEM)
|
||||
list_items(lib, decargs(args), opts.album, opts.path, fmt)
|
||||
list_cmd.func = list_func
|
||||
default_commands.append(list_cmd)
|
||||
|
||||
@@ -819,89 +859,89 @@ def update_items(lib, query, album, move, color, pretend):
|
||||
"""For all the items matched by the query, update the library to
|
||||
reflect the item's embedded tags.
|
||||
"""
|
||||
items, _ = _do_query(lib, query, album)
|
||||
with lib.transaction():
|
||||
items, _ = _do_query(lib, query, album)
|
||||
|
||||
# Walk through the items and pick up their changes.
|
||||
affected_albums = set()
|
||||
for item in items:
|
||||
# Item deleted?
|
||||
if not os.path.exists(syspath(item.path)):
|
||||
print_(u'X %s - %s' % (item.artist, item.title))
|
||||
if not pretend:
|
||||
lib.remove(item, True)
|
||||
affected_albums.add(item.album_id)
|
||||
continue
|
||||
|
||||
# Did the item change since last checked?
|
||||
if item.current_mtime() <= item.mtime:
|
||||
log.debug(u'skipping %s because mtime is up to date (%i)' %
|
||||
(displayable_path(item.path), item.mtime))
|
||||
continue
|
||||
|
||||
# Read new data.
|
||||
old_data = dict(item.record)
|
||||
item.read()
|
||||
|
||||
# Special-case album artist when it matches track artist. (Hacky
|
||||
# but necessary for preserving album-level metadata for non-
|
||||
# autotagged imports.)
|
||||
if not item.albumartist and \
|
||||
old_data['albumartist'] == old_data['artist'] == item.artist:
|
||||
item.albumartist = old_data['albumartist']
|
||||
item.dirty['albumartist'] = False
|
||||
|
||||
# Get and save metadata changes.
|
||||
changes = {}
|
||||
for key in library.ITEM_KEYS_META:
|
||||
if item.dirty[key]:
|
||||
changes[key] = old_data[key], getattr(item, key)
|
||||
if changes:
|
||||
# Something changed.
|
||||
print_(u'* %s - %s' % (item.artist, item.title))
|
||||
for key, (oldval, newval) in changes.iteritems():
|
||||
_showdiff(key, oldval, newval, color)
|
||||
|
||||
# If we're just pretending, then don't move or save.
|
||||
if pretend:
|
||||
# Walk through the items and pick up their changes.
|
||||
affected_albums = set()
|
||||
for item in items:
|
||||
# Item deleted?
|
||||
if not os.path.exists(syspath(item.path)):
|
||||
print_(u'X %s - %s' % (item.artist, item.title))
|
||||
if not pretend:
|
||||
lib.remove(item, True)
|
||||
affected_albums.add(item.album_id)
|
||||
continue
|
||||
|
||||
# Move the item if it's in the library.
|
||||
if move and lib.directory in ancestry(item.path):
|
||||
lib.move(item)
|
||||
# Did the item change since last checked?
|
||||
if item.current_mtime() <= item.mtime:
|
||||
log.debug(u'skipping %s because mtime is up to date (%i)' %
|
||||
(displayable_path(item.path), item.mtime))
|
||||
continue
|
||||
|
||||
lib.store(item)
|
||||
affected_albums.add(item.album_id)
|
||||
elif not pretend:
|
||||
# The file's mtime was different, but there were no changes
|
||||
# to the metadata. Store the new mtime, which is set in the
|
||||
# call to read(), so we don't check this again in the
|
||||
# future.
|
||||
lib.store(item)
|
||||
# Read new data.
|
||||
old_data = dict(item.record)
|
||||
item.read()
|
||||
|
||||
# Skip album changes while pretending.
|
||||
if pretend:
|
||||
return
|
||||
# Special-case album artist when it matches track artist. (Hacky
|
||||
# but necessary for preserving album-level metadata for non-
|
||||
# autotagged imports.)
|
||||
if not item.albumartist and \
|
||||
old_data['albumartist'] == old_data['artist'] == \
|
||||
item.artist:
|
||||
item.albumartist = old_data['albumartist']
|
||||
item.dirty['albumartist'] = False
|
||||
|
||||
# Modify affected albums to reflect changes in their items.
|
||||
for album_id in affected_albums:
|
||||
if album_id is None: # Singletons.
|
||||
continue
|
||||
album = lib.get_album(album_id)
|
||||
if not album: # Empty albums have already been removed.
|
||||
log.debug('emptied album %i' % album_id)
|
||||
continue
|
||||
al_items = list(album.items())
|
||||
# Get and save metadata changes.
|
||||
changes = {}
|
||||
for key in library.ITEM_KEYS_META:
|
||||
if item.dirty[key]:
|
||||
changes[key] = old_data[key], getattr(item, key)
|
||||
if changes:
|
||||
# Something changed.
|
||||
print_(u'* %s - %s' % (item.artist, item.title))
|
||||
for key, (oldval, newval) in changes.iteritems():
|
||||
_showdiff(key, oldval, newval, color)
|
||||
|
||||
# Update album structure to reflect an item in it.
|
||||
for key in library.ALBUM_KEYS_ITEM:
|
||||
setattr(album, key, getattr(al_items[0], key))
|
||||
# If we're just pretending, then don't move or save.
|
||||
if pretend:
|
||||
continue
|
||||
|
||||
# Move album art (and any inconsistent items).
|
||||
if move and lib.directory in ancestry(al_items[0].path):
|
||||
log.debug('moving album %i' % album_id)
|
||||
album.move()
|
||||
# Move the item if it's in the library.
|
||||
if move and lib.directory in ancestry(item.path):
|
||||
lib.move(item)
|
||||
|
||||
lib.save()
|
||||
lib.store(item)
|
||||
affected_albums.add(item.album_id)
|
||||
elif not pretend:
|
||||
# The file's mtime was different, but there were no changes
|
||||
# to the metadata. Store the new mtime, which is set in the
|
||||
# call to read(), so we don't check this again in the
|
||||
# future.
|
||||
lib.store(item)
|
||||
|
||||
# Skip album changes while pretending.
|
||||
if pretend:
|
||||
return
|
||||
|
||||
# Modify affected albums to reflect changes in their items.
|
||||
for album_id in affected_albums:
|
||||
if album_id is None: # Singletons.
|
||||
continue
|
||||
album = lib.get_album(album_id)
|
||||
if not album: # Empty albums have already been removed.
|
||||
log.debug('emptied album %i' % album_id)
|
||||
continue
|
||||
al_items = list(album.items())
|
||||
|
||||
# Update album structure to reflect an item in it.
|
||||
for key in library.ALBUM_KEYS_ITEM:
|
||||
setattr(album, key, getattr(al_items[0], key))
|
||||
|
||||
# Move album art (and any inconsistent items).
|
||||
if move and lib.directory in ancestry(al_items[0].path):
|
||||
log.debug('moving album %i' % album_id)
|
||||
album.move()
|
||||
|
||||
update_cmd = ui.Subcommand('update',
|
||||
help='update the library', aliases=('upd','up',))
|
||||
@@ -942,14 +982,13 @@ def remove_items(lib, query, album, delete=False):
|
||||
return
|
||||
|
||||
# Remove (and possibly delete) items.
|
||||
if album:
|
||||
for al in albums:
|
||||
al.remove(delete)
|
||||
else:
|
||||
for item in items:
|
||||
lib.remove(item, delete)
|
||||
|
||||
lib.save()
|
||||
with lib.transaction():
|
||||
if album:
|
||||
for al in albums:
|
||||
al.remove(delete)
|
||||
else:
|
||||
for item in items:
|
||||
lib.remove(item, delete)
|
||||
|
||||
remove_cmd = ui.Subcommand('remove',
|
||||
help='remove matching items from the library', aliases=('rm',))
|
||||
@@ -1007,16 +1046,18 @@ default_commands.append(stats_cmd)
|
||||
# version: Show current beets version.
|
||||
|
||||
def show_version(lib, config, opts, args):
|
||||
print 'beets version %s' % lib.beets.__version__
|
||||
|
||||
print_('beets version %s' % lib.beets.__version__)
|
||||
|
||||
# Show plugins.
|
||||
names = []
|
||||
for plugin in plugins.find_plugins():
|
||||
modname = plugin.__module__
|
||||
names.append(modname.split('.')[-1])
|
||||
if names:
|
||||
print 'plugins:', ', '.join(names)
|
||||
print_('plugins:', ', '.join(names))
|
||||
else:
|
||||
print 'no plugins loaded'
|
||||
print_('no plugins loaded')
|
||||
version_cmd = ui.Subcommand('version',
|
||||
help='output version information')
|
||||
version_cmd.func = show_version
|
||||
@@ -1061,23 +1102,23 @@ def modify_items(lib, mods, query, write, move, album, color, confirm):
|
||||
return
|
||||
|
||||
# Apply changes to database.
|
||||
for obj in objs:
|
||||
for field, value in fsets.iteritems():
|
||||
setattr(obj, field, value)
|
||||
with lib.transaction():
|
||||
for obj in objs:
|
||||
for field, value in fsets.iteritems():
|
||||
setattr(obj, field, value)
|
||||
|
||||
if move:
|
||||
cur_path = obj.item_dir() if album else obj.path
|
||||
if lib.directory in ancestry(cur_path): # In library?
|
||||
log.debug('moving object %s' % cur_path)
|
||||
if album:
|
||||
obj.move()
|
||||
else:
|
||||
lib.move(obj)
|
||||
if move:
|
||||
cur_path = obj.item_dir() if album else obj.path
|
||||
if lib.directory in ancestry(cur_path): # In library?
|
||||
log.debug('moving object %s' % cur_path)
|
||||
if album:
|
||||
obj.move()
|
||||
else:
|
||||
lib.move(obj)
|
||||
|
||||
# When modifying items, we have to store them to the database.
|
||||
if not album:
|
||||
lib.store(obj)
|
||||
lib.save()
|
||||
# When modifying items, we have to store them to the database.
|
||||
if not album:
|
||||
lib.store(obj)
|
||||
|
||||
# Apply tags if requested.
|
||||
if write:
|
||||
@@ -1136,7 +1177,6 @@ def move_items(lib, dest, query, copy, album):
|
||||
else:
|
||||
lib.move(obj, copy, basedir=dest)
|
||||
lib.store(obj)
|
||||
lib.save()
|
||||
|
||||
move_cmd = ui.Subcommand('move',
|
||||
help='move or copy items', aliases=('mv',))
|
||||
|
||||
Reference in New Issue
Block a user