mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-21 00:14:02 +01:00
mutagen, musicbrainzngs, beets lib updates
This commit is contained in:
Regular → Executable
+462
-184
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of beets.
|
||||
# Copyright 2014, Adrian Sampson.
|
||||
# Copyright 2016, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -16,28 +17,31 @@
|
||||
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 locale
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import optparse
|
||||
import textwrap
|
||||
import sys
|
||||
from difflib import SequenceMatcher
|
||||
import logging
|
||||
import sqlite3
|
||||
import errno
|
||||
import re
|
||||
import struct
|
||||
import traceback
|
||||
import os.path
|
||||
from six.moves import input
|
||||
|
||||
from beets import logging
|
||||
from beets import library
|
||||
from beets import plugins
|
||||
from beets import util
|
||||
from beets.util.functemplate import Template
|
||||
from beets import config
|
||||
from beets.util import confit
|
||||
from beets.util import confit, as_string
|
||||
from beets.autotag import mb
|
||||
from beets.dbcore import query as db_query
|
||||
import six
|
||||
|
||||
# On Windows platforms, use colorama to support "ANSI" terminal colors.
|
||||
if sys.platform == 'win32':
|
||||
@@ -56,8 +60,8 @@ log.propagate = False # Don't propagate to root handler.
|
||||
|
||||
|
||||
PF_KEY_QUERIES = {
|
||||
'comp': 'comp:true',
|
||||
'singleton': 'singleton:true',
|
||||
'comp': u'comp:true',
|
||||
'singleton': u'singleton:true',
|
||||
}
|
||||
|
||||
|
||||
@@ -67,68 +71,149 @@ class UserError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
# Utilities.
|
||||
# Encoding utilities.
|
||||
|
||||
def _encoding():
|
||||
"""Tries to guess the encoding used by the terminal."""
|
||||
|
||||
def _in_encoding():
|
||||
"""Get the encoding to use for *inputting* strings from the console.
|
||||
"""
|
||||
return _stream_encoding(sys.stdin)
|
||||
|
||||
|
||||
def _out_encoding():
|
||||
"""Get the encoding to use for *outputting* strings to the console.
|
||||
"""
|
||||
return _stream_encoding(sys.stdout)
|
||||
|
||||
|
||||
def _stream_encoding(stream, default='utf-8'):
|
||||
"""A helper for `_in_encoding` and `_out_encoding`: get the stream's
|
||||
preferred encoding, using a configured override or a default
|
||||
fallback if neither is not specified.
|
||||
"""
|
||||
# Configured override?
|
||||
encoding = config['terminal_encoding'].get()
|
||||
if encoding:
|
||||
return encoding
|
||||
|
||||
# Determine from locale settings.
|
||||
try:
|
||||
return locale.getdefaultlocale()[1] or 'utf8'
|
||||
except ValueError:
|
||||
# Invalid locale environment variable setting. To avoid
|
||||
# failing entirely for no good reason, assume UTF-8.
|
||||
return 'utf8'
|
||||
# For testing: When sys.stdout or sys.stdin is a StringIO under the
|
||||
# test harness, it doesn't have an `encoding` attribute. Just use
|
||||
# UTF-8.
|
||||
if not hasattr(stream, 'encoding'):
|
||||
return default
|
||||
|
||||
# Python's guessed output stream encoding, or UTF-8 as a fallback
|
||||
# (e.g., when piped to a file).
|
||||
return stream.encoding or default
|
||||
|
||||
|
||||
def decargs(arglist):
|
||||
"""Given a list of command-line argument bytestrings, attempts to
|
||||
decode them to Unicode strings.
|
||||
decode them to Unicode strings when running under Python 2.
|
||||
"""
|
||||
return [s.decode(_encoding()) for s in arglist]
|
||||
if six.PY2:
|
||||
return [s.decode(util.arg_encoding()) for s in arglist]
|
||||
else:
|
||||
return arglist
|
||||
|
||||
|
||||
def print_(*strings):
|
||||
def print_(*strings, **kwargs):
|
||||
"""Like print, but rather than raising an error when a character
|
||||
is not in the terminal's encoding's character set, just silently
|
||||
replaces it.
|
||||
"""
|
||||
if strings:
|
||||
if isinstance(strings[0], unicode):
|
||||
txt = u' '.join(strings)
|
||||
else:
|
||||
txt = ' '.join(strings)
|
||||
else:
|
||||
txt = u''
|
||||
if isinstance(txt, unicode):
|
||||
txt = txt.encode(_encoding(), 'replace')
|
||||
print(txt)
|
||||
|
||||
The arguments must be Unicode strings: `unicode` on Python 2; `str` on
|
||||
Python 3.
|
||||
|
||||
The `end` keyword argument behaves similarly to the built-in `print`
|
||||
(it defaults to a newline).
|
||||
"""
|
||||
if not strings:
|
||||
strings = [u'']
|
||||
assert isinstance(strings[0], six.text_type)
|
||||
|
||||
txt = u' '.join(strings)
|
||||
txt += kwargs.get('end', u'\n')
|
||||
|
||||
# Encode the string and write it to stdout.
|
||||
if six.PY2:
|
||||
# On Python 2, sys.stdout expects bytes.
|
||||
out = txt.encode(_out_encoding(), 'replace')
|
||||
sys.stdout.write(out)
|
||||
else:
|
||||
# On Python 3, sys.stdout expects text strings and uses the
|
||||
# exception-throwing encoding error policy. To avoid throwing
|
||||
# errors and use our configurable encoding override, we use the
|
||||
# underlying bytes buffer instead.
|
||||
if hasattr(sys.stdout, 'buffer'):
|
||||
out = txt.encode(_out_encoding(), 'replace')
|
||||
sys.stdout.buffer.write(out)
|
||||
else:
|
||||
# In our test harnesses (e.g., DummyOut), sys.stdout.buffer
|
||||
# does not exist. We instead just record the text string.
|
||||
sys.stdout.write(txt)
|
||||
|
||||
|
||||
# Configuration wrappers.
|
||||
|
||||
def _bool_fallback(a, b):
|
||||
"""Given a boolean or None, return the original value or a fallback.
|
||||
"""
|
||||
if a is None:
|
||||
assert isinstance(b, bool)
|
||||
return b
|
||||
else:
|
||||
assert isinstance(a, bool)
|
||||
return a
|
||||
|
||||
|
||||
def should_write(write_opt=None):
|
||||
"""Decide whether a command that updates metadata should also write
|
||||
tags, using the importer configuration as the default.
|
||||
"""
|
||||
return _bool_fallback(write_opt, config['import']['write'].get(bool))
|
||||
|
||||
|
||||
def should_move(move_opt=None):
|
||||
"""Decide whether a command that updates metadata should also move
|
||||
files when they're inside the library, using the importer
|
||||
configuration as the default.
|
||||
|
||||
Specifically, commands should move files after metadata updates only
|
||||
when the importer is configured *either* to move *or* to copy files.
|
||||
They should avoid moving files when the importer is configured not
|
||||
to touch any filenames.
|
||||
"""
|
||||
return _bool_fallback(
|
||||
move_opt,
|
||||
config['import']['move'].get(bool) or
|
||||
config['import']['copy'].get(bool)
|
||||
)
|
||||
|
||||
|
||||
# Input prompts.
|
||||
|
||||
def input_(prompt=None):
|
||||
"""Like `raw_input`, but decodes the result to a Unicode string.
|
||||
"""Like `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.
|
||||
# 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=' ')
|
||||
print_(prompt, end=u' ')
|
||||
|
||||
try:
|
||||
resp = raw_input()
|
||||
resp = input()
|
||||
except EOFError:
|
||||
raise UserError('stdin stream ended while input required')
|
||||
raise UserError(u'stdin stream ended while input required')
|
||||
|
||||
return resp.decode(sys.stdin.encoding or 'utf8', 'ignore')
|
||||
if six.PY2:
|
||||
return resp.decode(_in_encoding(), 'ignore')
|
||||
else:
|
||||
return resp
|
||||
|
||||
|
||||
def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
@@ -172,7 +257,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
found_letter = letter
|
||||
break
|
||||
else:
|
||||
raise ValueError('no unambiguous lettering found')
|
||||
raise ValueError(u'no unambiguous lettering found')
|
||||
|
||||
letters[found_letter.lower()] = option
|
||||
index = option.index(found_letter)
|
||||
@@ -180,7 +265,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
# Mark the option's shortcut letter for display.
|
||||
if not require and (
|
||||
(default is None and not numrange and first) or
|
||||
(isinstance(default, basestring) and
|
||||
(isinstance(default, six.string_types) and
|
||||
found_letter.lower() == default.lower())):
|
||||
# The first option is the default; mark it.
|
||||
show_letter = '[%s]' % found_letter.upper()
|
||||
@@ -190,7 +275,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
is_default = False
|
||||
|
||||
# Colorize the letter shortcut.
|
||||
show_letter = colorize('turquoise' if is_default else 'blue',
|
||||
show_letter = colorize('action_default' if is_default else 'action',
|
||||
show_letter)
|
||||
|
||||
# Insert the highlighted letter back into the word.
|
||||
@@ -216,11 +301,11 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
prompt_part_lengths = []
|
||||
if numrange:
|
||||
if isinstance(default, int):
|
||||
default_name = str(default)
|
||||
default_name = colorize('turquoise', default_name)
|
||||
default_name = six.text_type(default)
|
||||
default_name = colorize('action_default', default_name)
|
||||
tmpl = '# selection (default %s)'
|
||||
prompt_parts.append(tmpl % default_name)
|
||||
prompt_part_lengths.append(len(tmpl % str(default)))
|
||||
prompt_part_lengths.append(len(tmpl % six.text_type(default)))
|
||||
else:
|
||||
prompt_parts.append('# selection')
|
||||
prompt_part_lengths.append(len(prompt_parts[-1]))
|
||||
@@ -255,9 +340,9 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
# Make a fallback prompt too. This is displayed if the user enters
|
||||
# something that is not recognized.
|
||||
if not fallback_prompt:
|
||||
fallback_prompt = 'Enter one of '
|
||||
fallback_prompt = u'Enter one of '
|
||||
if numrange:
|
||||
fallback_prompt += '%i-%i, ' % numrange
|
||||
fallback_prompt += u'%i-%i, ' % numrange
|
||||
fallback_prompt += ', '.join(display_letters) + ':'
|
||||
|
||||
resp = input_(prompt)
|
||||
@@ -296,19 +381,52 @@ def input_yn(prompt, require=False):
|
||||
"yes" unless `require` is `True`, in which case there is no default.
|
||||
"""
|
||||
sel = input_options(
|
||||
('y', 'n'), require, prompt, 'Enter Y or N:'
|
||||
('y', 'n'), require, prompt, u'Enter Y or N:'
|
||||
)
|
||||
return sel == 'y'
|
||||
return sel == u'y'
|
||||
|
||||
|
||||
def input_select_objects(prompt, objs, rep):
|
||||
"""Prompt to user to choose all, none, or some of the given objects.
|
||||
Return the list of selected objects.
|
||||
|
||||
`prompt` is the prompt string to use for each question (it should be
|
||||
phrased as an imperative verb). `rep` is a function to call on each
|
||||
object to print it out when confirming objects individually.
|
||||
"""
|
||||
choice = input_options(
|
||||
(u'y', u'n', u's'), False,
|
||||
u'%s? (Yes/no/select)' % prompt)
|
||||
print() # Blank line.
|
||||
|
||||
if choice == u'y': # Yes.
|
||||
return objs
|
||||
|
||||
elif choice == u's': # Select.
|
||||
out = []
|
||||
for obj in objs:
|
||||
rep(obj)
|
||||
if input_yn(u'%s? (yes/no)' % prompt, True):
|
||||
out.append(obj)
|
||||
print() # go to a new line
|
||||
return out
|
||||
|
||||
else: # No.
|
||||
return []
|
||||
|
||||
|
||||
# Human output formatting.
|
||||
|
||||
def human_bytes(size):
|
||||
"""Formats size, a number of bytes, in a human-readable way."""
|
||||
suffices = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'HB']
|
||||
for suffix in suffices:
|
||||
powers = [u'', u'K', u'M', u'G', u'T', u'P', u'E', u'Z', u'Y', u'H']
|
||||
unit = 'B'
|
||||
for power in powers:
|
||||
if size < 1024:
|
||||
return "%3.1f %s" % (size, suffix)
|
||||
return u"%3.1f %s%s" % (size, power, unit)
|
||||
size /= 1024.0
|
||||
return "big"
|
||||
unit = u'iB'
|
||||
return u"big"
|
||||
|
||||
|
||||
def human_seconds(interval):
|
||||
@@ -316,13 +434,13 @@ def human_seconds(interval):
|
||||
interval using English words.
|
||||
"""
|
||||
units = [
|
||||
(1, 'second'),
|
||||
(60, 'minute'),
|
||||
(60, 'hour'),
|
||||
(24, 'day'),
|
||||
(7, 'week'),
|
||||
(52, 'year'),
|
||||
(10, 'decade'),
|
||||
(1, u'second'),
|
||||
(60, u'minute'),
|
||||
(60, u'hour'),
|
||||
(24, u'day'),
|
||||
(7, u'week'),
|
||||
(52, u'year'),
|
||||
(10, u'decade'),
|
||||
]
|
||||
for i in range(len(units) - 1):
|
||||
increment, suffix = units[i]
|
||||
@@ -335,7 +453,7 @@ def human_seconds(interval):
|
||||
increment, suffix = units[-1]
|
||||
interval /= float(increment)
|
||||
|
||||
return "%3.1f %ss" % (interval, suffix)
|
||||
return u"%3.1f %ss" % (interval, suffix)
|
||||
|
||||
|
||||
def human_seconds_short(interval):
|
||||
@@ -346,16 +464,45 @@ def human_seconds_short(interval):
|
||||
return u'%i:%02i' % (interval // 60, interval % 60)
|
||||
|
||||
|
||||
# Colorization.
|
||||
|
||||
# ANSI terminal colorization code heavily inspired by pygments:
|
||||
# http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py
|
||||
# (pygments is by Tim Hatch, Armin Ronacher, et al.)
|
||||
COLOR_ESCAPE = "\x1b["
|
||||
DARK_COLORS = ["black", "darkred", "darkgreen", "brown", "darkblue",
|
||||
"purple", "teal", "lightgray"]
|
||||
LIGHT_COLORS = ["darkgray", "red", "green", "yellow", "blue",
|
||||
"fuchsia", "turquoise", "white"]
|
||||
DARK_COLORS = {
|
||||
"black": 0,
|
||||
"darkred": 1,
|
||||
"darkgreen": 2,
|
||||
"brown": 3,
|
||||
"darkyellow": 3,
|
||||
"darkblue": 4,
|
||||
"purple": 5,
|
||||
"darkmagenta": 5,
|
||||
"teal": 6,
|
||||
"darkcyan": 6,
|
||||
"lightgray": 7
|
||||
}
|
||||
LIGHT_COLORS = {
|
||||
"darkgray": 0,
|
||||
"red": 1,
|
||||
"green": 2,
|
||||
"yellow": 3,
|
||||
"blue": 4,
|
||||
"fuchsia": 5,
|
||||
"magenta": 5,
|
||||
"turquoise": 6,
|
||||
"cyan": 6,
|
||||
"white": 7
|
||||
}
|
||||
RESET_COLOR = COLOR_ESCAPE + "39;49;00m"
|
||||
|
||||
# These abstract COLOR_NAMES are lazily mapped on to the actual color in COLORS
|
||||
# as they are defined in the configuration files, see function: colorize
|
||||
COLOR_NAMES = ['text_success', 'text_warning', 'text_error', 'text_highlight',
|
||||
'text_highlight_minor', 'action_default', 'action']
|
||||
COLORS = None
|
||||
|
||||
|
||||
def _colorize(color, text):
|
||||
"""Returns a string that prints the given text in the given color
|
||||
@@ -363,34 +510,47 @@ def _colorize(color, text):
|
||||
in DARK_COLORS or LIGHT_COLORS.
|
||||
"""
|
||||
if color in DARK_COLORS:
|
||||
escape = COLOR_ESCAPE + "%im" % (DARK_COLORS.index(color) + 30)
|
||||
escape = COLOR_ESCAPE + "%im" % (DARK_COLORS[color] + 30)
|
||||
elif color in LIGHT_COLORS:
|
||||
escape = COLOR_ESCAPE + "%i;01m" % (LIGHT_COLORS.index(color) + 30)
|
||||
escape = COLOR_ESCAPE + "%i;01m" % (LIGHT_COLORS[color] + 30)
|
||||
else:
|
||||
raise ValueError('no such color %s', color)
|
||||
raise ValueError(u'no such color %s', color)
|
||||
return escape + text + RESET_COLOR
|
||||
|
||||
|
||||
def colorize(color, text):
|
||||
def colorize(color_name, text):
|
||||
"""Colorize text if colored output is enabled. (Like _colorize but
|
||||
conditional.)
|
||||
"""
|
||||
if config['color']:
|
||||
if config['ui']['color']:
|
||||
global COLORS
|
||||
if not COLORS:
|
||||
COLORS = dict((name,
|
||||
config['ui']['colors'][name].as_str())
|
||||
for name in COLOR_NAMES)
|
||||
# In case a 3rd party plugin is still passing the actual color ('red')
|
||||
# instead of the abstract color name ('text_error')
|
||||
color = COLORS.get(color_name)
|
||||
if not color:
|
||||
log.debug(u'Invalid color_name: {0}', color_name)
|
||||
color = color_name
|
||||
return _colorize(color, text)
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def _colordiff(a, b, highlight='red', minor_highlight='lightgray'):
|
||||
def _colordiff(a, b, highlight='text_highlight',
|
||||
minor_highlight='text_highlight_minor'):
|
||||
"""Given two values, return the same pair of strings except with
|
||||
their differences highlighted in the specified color. Strings are
|
||||
highlighted intelligently to show differences; other values are
|
||||
stringified and highlighted in their entirety.
|
||||
"""
|
||||
if not isinstance(a, basestring) or not isinstance(b, basestring):
|
||||
if not isinstance(a, six.string_types) \
|
||||
or not isinstance(b, six.string_types):
|
||||
# Non-strings: use ordinary equality.
|
||||
a = unicode(a)
|
||||
b = unicode(b)
|
||||
a = six.text_type(a)
|
||||
b = six.text_type(b)
|
||||
if a == b:
|
||||
return a, b
|
||||
else:
|
||||
@@ -431,14 +591,14 @@ def _colordiff(a, b, highlight='red', minor_highlight='lightgray'):
|
||||
return u''.join(a_out), u''.join(b_out)
|
||||
|
||||
|
||||
def colordiff(a, b, highlight='red'):
|
||||
def colordiff(a, b, highlight='text_highlight'):
|
||||
"""Colorize differences between two values if color is enabled.
|
||||
(Like _colordiff but conditional.)
|
||||
"""
|
||||
if config['color']:
|
||||
if config['ui']['color']:
|
||||
return _colordiff(a, b, highlight)
|
||||
else:
|
||||
return unicode(a), unicode(b)
|
||||
return six.text_type(a), six.text_type(b)
|
||||
|
||||
|
||||
def get_path_formats(subview=None):
|
||||
@@ -449,7 +609,7 @@ def get_path_formats(subview=None):
|
||||
subview = subview or config['paths']
|
||||
for query, view in subview.items():
|
||||
query = PF_KEY_QUERIES.get(query, query) # Expand common queries.
|
||||
path_formats.append((query, Template(view.get(unicode))))
|
||||
path_formats.append((query, Template(view.as_str())))
|
||||
return path_formats
|
||||
|
||||
|
||||
@@ -470,31 +630,6 @@ def get_replacements():
|
||||
return replacements
|
||||
|
||||
|
||||
def _pick_format(album, fmt=None):
|
||||
"""Pick a format string for printing Album or Item objects,
|
||||
falling back to config options and defaults.
|
||||
"""
|
||||
if fmt:
|
||||
return fmt
|
||||
if album:
|
||||
return config['list_format_album'].get(unicode)
|
||||
else:
|
||||
return config['list_format_item'].get(unicode)
|
||||
|
||||
|
||||
def print_obj(obj, lib, fmt=None):
|
||||
"""Print an Album or Item object. If `fmt` is specified, use that
|
||||
format string. Otherwise, use the configured template.
|
||||
"""
|
||||
album = isinstance(obj, library.Album)
|
||||
fmt = _pick_format(album, fmt)
|
||||
if isinstance(fmt, Template):
|
||||
template = fmt
|
||||
else:
|
||||
template = Template(fmt)
|
||||
print_(obj.evaluate_template(template))
|
||||
|
||||
|
||||
def term_width():
|
||||
"""Get the width (columns) of the terminal."""
|
||||
fallback = config['ui']['terminal_width'].get(int)
|
||||
@@ -542,10 +677,11 @@ def _field_diff(field, old, new):
|
||||
|
||||
# For strings, highlight changes. For others, colorize the whole
|
||||
# thing.
|
||||
if isinstance(oldval, basestring):
|
||||
if isinstance(oldval, six.string_types):
|
||||
oldstr, newstr = colordiff(oldval, newstr)
|
||||
else:
|
||||
oldstr, newstr = colorize('red', oldstr), colorize('red', newstr)
|
||||
oldstr = colorize('text_error', oldstr)
|
||||
newstr = colorize('text_error', newstr)
|
||||
|
||||
return u'{0} -> {1}'.format(oldstr, newstr)
|
||||
|
||||
@@ -581,18 +717,178 @@ def show_model_changes(new, old=None, fields=None, always=False):
|
||||
|
||||
changes.append(u' {0}: {1}'.format(
|
||||
field,
|
||||
colorize('red', new.formatted()[field])
|
||||
colorize('text_highlight', new.formatted()[field])
|
||||
))
|
||||
|
||||
# Print changes.
|
||||
if changes or always:
|
||||
print_obj(old, old._db)
|
||||
print_(format(old))
|
||||
if changes:
|
||||
print_(u'\n'.join(changes))
|
||||
|
||||
return bool(changes)
|
||||
|
||||
|
||||
def show_path_changes(path_changes):
|
||||
"""Given a list of tuples (source, destination) that indicate the
|
||||
path changes, log the changes as INFO-level output to the beets log.
|
||||
The output is guaranteed to be unicode.
|
||||
|
||||
Every pair is shown on a single line if the terminal width permits it,
|
||||
else it is split over two lines. E.g.,
|
||||
|
||||
Source -> Destination
|
||||
|
||||
vs.
|
||||
|
||||
Source
|
||||
-> Destination
|
||||
"""
|
||||
sources, destinations = zip(*path_changes)
|
||||
|
||||
# Ensure unicode output
|
||||
sources = list(map(util.displayable_path, sources))
|
||||
destinations = list(map(util.displayable_path, destinations))
|
||||
|
||||
# Calculate widths for terminal split
|
||||
col_width = (term_width() - len(' -> ')) // 2
|
||||
max_width = len(max(sources + destinations, key=len))
|
||||
|
||||
if max_width > col_width:
|
||||
# Print every change over two lines
|
||||
for source, dest in zip(sources, destinations):
|
||||
log.info(u'{0} \n -> {1}', source, dest)
|
||||
else:
|
||||
# Print every change on a single line, and add a header
|
||||
title_pad = max_width - len('Source ') + len(' -> ')
|
||||
|
||||
log.info(u'Source {0} Destination', ' ' * title_pad)
|
||||
for source, dest in zip(sources, destinations):
|
||||
pad = max_width - len(source)
|
||||
log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest)
|
||||
|
||||
|
||||
class CommonOptionsParser(optparse.OptionParser, object):
|
||||
"""Offers a simple way to add common formatting options.
|
||||
|
||||
Options available include:
|
||||
- matching albums instead of tracks: add_album_option()
|
||||
- showing paths instead of items/albums: add_path_option()
|
||||
- changing the format of displayed items/albums: add_format_option()
|
||||
|
||||
The last one can have several behaviors:
|
||||
- against a special target
|
||||
- with a certain format
|
||||
- autodetected target with the album option
|
||||
|
||||
Each method is fully documented in the related method.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CommonOptionsParser, self).__init__(*args, **kwargs)
|
||||
self._album_flags = False
|
||||
# this serves both as an indicator that we offer the feature AND allows
|
||||
# us to check whether it has been specified on the CLI - bypassing the
|
||||
# fact that arguments may be in any order
|
||||
|
||||
def add_album_option(self, flags=('-a', '--album')):
|
||||
"""Add a -a/--album option to match albums instead of tracks.
|
||||
|
||||
If used then the format option can auto-detect whether we're setting
|
||||
the format for items or albums.
|
||||
Sets the album property on the options extracted from the CLI.
|
||||
"""
|
||||
album = optparse.Option(*flags, action='store_true',
|
||||
help=u'match albums instead of tracks')
|
||||
self.add_option(album)
|
||||
self._album_flags = set(flags)
|
||||
|
||||
def _set_format(self, option, opt_str, value, parser, target=None,
|
||||
fmt=None, store_true=False):
|
||||
"""Internal callback that sets the correct format while parsing CLI
|
||||
arguments.
|
||||
"""
|
||||
if store_true:
|
||||
setattr(parser.values, option.dest, True)
|
||||
|
||||
# Use the explicitly specified format, or the string from the option.
|
||||
if fmt:
|
||||
value = fmt
|
||||
elif value:
|
||||
value, = decargs([value])
|
||||
else:
|
||||
value = u''
|
||||
|
||||
parser.values.format = value
|
||||
if target:
|
||||
config[target._format_config_key].set(value)
|
||||
else:
|
||||
if self._album_flags:
|
||||
if parser.values.album:
|
||||
target = library.Album
|
||||
else:
|
||||
# the option is either missing either not parsed yet
|
||||
if self._album_flags & set(parser.rargs):
|
||||
target = library.Album
|
||||
else:
|
||||
target = library.Item
|
||||
config[target._format_config_key].set(value)
|
||||
else:
|
||||
config[library.Item._format_config_key].set(value)
|
||||
config[library.Album._format_config_key].set(value)
|
||||
|
||||
def add_path_option(self, flags=('-p', '--path')):
|
||||
"""Add a -p/--path option to display the path instead of the default
|
||||
format.
|
||||
|
||||
By default this affects both items and albums. If add_album_option()
|
||||
is used then the target will be autodetected.
|
||||
|
||||
Sets the format property to u'$path' on the options extracted from the
|
||||
CLI.
|
||||
"""
|
||||
path = optparse.Option(*flags, nargs=0, action='callback',
|
||||
callback=self._set_format,
|
||||
callback_kwargs={'fmt': u'$path',
|
||||
'store_true': True},
|
||||
help=u'print paths for matched items or albums')
|
||||
self.add_option(path)
|
||||
|
||||
def add_format_option(self, flags=('-f', '--format'), target=None):
|
||||
"""Add -f/--format option to print some LibModel instances with a
|
||||
custom format.
|
||||
|
||||
`target` is optional and can be one of ``library.Item``, 'item',
|
||||
``library.Album`` and 'album'.
|
||||
|
||||
Several behaviors are available:
|
||||
- if `target` is given then the format is only applied to that
|
||||
LibModel
|
||||
- if the album option is used then the target will be autodetected
|
||||
- otherwise the format is applied to both items and albums.
|
||||
|
||||
Sets the format property on the options extracted from the CLI.
|
||||
"""
|
||||
kwargs = {}
|
||||
if target:
|
||||
if isinstance(target, six.string_types):
|
||||
target = {'item': library.Item,
|
||||
'album': library.Album}[target]
|
||||
kwargs['target'] = target
|
||||
|
||||
opt = optparse.Option(*flags, action='callback',
|
||||
callback=self._set_format,
|
||||
callback_kwargs=kwargs,
|
||||
help=u'print with custom format')
|
||||
self.add_option(opt)
|
||||
|
||||
def add_all_common_options(self):
|
||||
"""Add album, path and format options.
|
||||
"""
|
||||
self.add_album_option()
|
||||
self.add_path_option()
|
||||
self.add_format_option()
|
||||
|
||||
|
||||
# Subcommand parsing infrastructure.
|
||||
#
|
||||
# This is a fairly generic subcommand parser for optparse. It is
|
||||
@@ -610,10 +906,10 @@ class Subcommand(object):
|
||||
the subcommand; aliases are alternate names. parser is an
|
||||
OptionParser responsible for parsing the subcommand's options.
|
||||
help is a short description of the command. If no parser is
|
||||
given, it defaults to a new, empty OptionParser.
|
||||
given, it defaults to a new, empty CommonOptionsParser.
|
||||
"""
|
||||
self.name = name
|
||||
self.parser = parser or optparse.OptionParser()
|
||||
self.parser = parser or CommonOptionsParser()
|
||||
self.aliases = aliases
|
||||
self.help = help
|
||||
self.hide = hide
|
||||
@@ -632,11 +928,11 @@ class Subcommand(object):
|
||||
@root_parser.setter
|
||||
def root_parser(self, root_parser):
|
||||
self._root_parser = root_parser
|
||||
self.parser.prog = '{0} {1}'.format(root_parser.get_prog_name(),
|
||||
self.name)
|
||||
self.parser.prog = '{0} {1}'.format(
|
||||
as_string(root_parser.get_prog_name()), self.name)
|
||||
|
||||
|
||||
class SubcommandsOptionParser(optparse.OptionParser):
|
||||
class SubcommandsOptionParser(CommonOptionsParser):
|
||||
"""A variant of OptionParser that parses subcommands and their
|
||||
arguments.
|
||||
"""
|
||||
@@ -648,13 +944,13 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
"""
|
||||
# A more helpful default usage.
|
||||
if 'usage' not in kwargs:
|
||||
kwargs['usage'] = """
|
||||
kwargs['usage'] = u"""
|
||||
%prog COMMAND [ARGS...]
|
||||
%prog help COMMAND"""
|
||||
kwargs['add_help_option'] = False
|
||||
|
||||
# Super constructor.
|
||||
optparse.OptionParser.__init__(self, *args, **kwargs)
|
||||
super(SubcommandsOptionParser, self).__init__(*args, **kwargs)
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
self.disable_interspersed_args()
|
||||
@@ -671,7 +967,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
# 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)
|
||||
out = super(SubcommandsOptionParser, self).format_help(formatter)
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
|
||||
@@ -711,7 +1007,8 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
result.append(name)
|
||||
help_width = formatter.width - help_position
|
||||
help_lines = textwrap.wrap(subcommand.help, help_width)
|
||||
result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
|
||||
help_line = help_lines[0] if help_lines else ''
|
||||
result.append("%*s%s\n" % (indent_first, "", help_line))
|
||||
result.extend(["%*s%s\n" % (help_position, "", line)
|
||||
for line in help_lines[1:]])
|
||||
formatter.dedent()
|
||||
@@ -756,7 +1053,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
cmdname = args.pop(0)
|
||||
subcommand = self._subcommand_for_name(cmdname)
|
||||
if not subcommand:
|
||||
raise UserError("unknown command '{0}'".format(cmdname))
|
||||
raise UserError(u"unknown command '{0}'".format(cmdname))
|
||||
|
||||
suboptions, subargs = subcommand.parse_args(args)
|
||||
return subcommand, suboptions, subargs
|
||||
@@ -765,53 +1062,24 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
optparse.Option.ALWAYS_TYPED_ACTIONS += ('callback',)
|
||||
|
||||
|
||||
def vararg_callback(option, opt_str, value, parser):
|
||||
"""Callback for an option with variable arguments.
|
||||
Manually collect arguments right of a callback-action
|
||||
option (ie. with action="callback"), and add the resulting
|
||||
list to the destination var.
|
||||
|
||||
Usage:
|
||||
parser.add_option("-c", "--callback", dest="vararg_attr",
|
||||
action="callback", callback=vararg_callback)
|
||||
|
||||
Details:
|
||||
http://docs.python.org/2/library/optparse.html#callback-example-6-variable
|
||||
-arguments
|
||||
"""
|
||||
value = [value]
|
||||
|
||||
def floatable(str):
|
||||
try:
|
||||
float(str)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
for arg in parser.rargs:
|
||||
# stop on --foo like options
|
||||
if arg[:2] == "--" and len(arg) > 2:
|
||||
break
|
||||
# stop on -a, but not on -3 or -3.0
|
||||
if arg[:1] == "-" and len(arg) > 1 and not floatable(arg):
|
||||
break
|
||||
value.append(arg)
|
||||
|
||||
del parser.rargs[:len(value) - 1]
|
||||
setattr(parser.values, option.dest, value)
|
||||
|
||||
|
||||
# The main entry point and bootstrapping.
|
||||
|
||||
def _load_plugins(config):
|
||||
"""Load the plugins specified in the configuration.
|
||||
"""
|
||||
paths = config['pluginpath'].get(confit.StrSeq(split=False))
|
||||
paths = map(util.normpath, paths)
|
||||
paths = config['pluginpath'].as_str_seq(split=False)
|
||||
paths = [util.normpath(p) for p in paths]
|
||||
log.debug(u'plugin paths: {0}', util.displayable_path(paths))
|
||||
|
||||
# On Python 3, the search paths need to be unicode.
|
||||
paths = [util.py3_path(p) for p in paths]
|
||||
|
||||
# Extend the `beetsplug` package to include the plugin paths.
|
||||
import beetsplug
|
||||
beetsplug.__path__ = paths + beetsplug.__path__
|
||||
# For backwards compatibility.
|
||||
|
||||
# For backwards compatibility, also support plugin paths that
|
||||
# *contain* a `beetsplug` package.
|
||||
sys.path += paths
|
||||
|
||||
plugins.load_plugins(config['plugins'].as_str_seq())
|
||||
@@ -840,8 +1108,8 @@ def _setup(options, lib=None):
|
||||
if lib is None:
|
||||
lib = _open_library(config)
|
||||
plugins.send("library_opened", lib=lib)
|
||||
library.Item._types = plugins.types(library.Item)
|
||||
library.Album._types = plugins.types(library.Album)
|
||||
library.Item._types.update(plugins.types(library.Item))
|
||||
library.Album._types.update(plugins.types(library.Album))
|
||||
|
||||
return subcommands, plugins, lib
|
||||
|
||||
@@ -859,28 +1127,28 @@ def _configure(options):
|
||||
config.set_args(options)
|
||||
|
||||
# Configure the logger.
|
||||
if config['verbose'].get(bool):
|
||||
log.setLevel(logging.DEBUG)
|
||||
if config['verbose'].get(int):
|
||||
log.set_global_level(logging.DEBUG)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
log.set_global_level(logging.INFO)
|
||||
|
||||
config_path = config.user_config_path()
|
||||
if os.path.isfile(config_path):
|
||||
log.debug(u'user configuration: {0}'.format(
|
||||
util.displayable_path(config_path)))
|
||||
log.debug(u'user configuration: {0}',
|
||||
util.displayable_path(config_path))
|
||||
else:
|
||||
log.debug(u'no user configuration found at {0}'.format(
|
||||
util.displayable_path(config_path)))
|
||||
log.debug(u'no user configuration found at {0}',
|
||||
util.displayable_path(config_path))
|
||||
|
||||
log.debug(u'data directory: {0}'
|
||||
.format(util.displayable_path(config.config_dir())))
|
||||
log.debug(u'data directory: {0}',
|
||||
util.displayable_path(config.config_dir()))
|
||||
return config
|
||||
|
||||
|
||||
def _open_library(config):
|
||||
"""Create a new library instance from the configuration.
|
||||
"""
|
||||
dbpath = config['library'].as_filename()
|
||||
dbpath = util.bytestring_path(config['library'].as_filename())
|
||||
try:
|
||||
lib = library.Library(
|
||||
dbpath,
|
||||
@@ -890,14 +1158,14 @@ def _open_library(config):
|
||||
)
|
||||
lib.get_item(0) # Test database connection.
|
||||
except (sqlite3.OperationalError, sqlite3.DatabaseError):
|
||||
log.debug(traceback.format_exc())
|
||||
log.debug(u'{}', traceback.format_exc())
|
||||
raise UserError(u"database file {0} could not be opened".format(
|
||||
util.displayable_path(dbpath)
|
||||
))
|
||||
log.debug(u'library database: {0}\n'
|
||||
u'library directory: {1}'
|
||||
.format(util.displayable_path(lib.path),
|
||||
util.displayable_path(lib.directory)))
|
||||
u'library directory: {1}',
|
||||
util.displayable_path(lib.path),
|
||||
util.displayable_path(lib.directory))
|
||||
return lib
|
||||
|
||||
|
||||
@@ -906,16 +1174,18 @@ def _raw_main(args, lib=None):
|
||||
handling.
|
||||
"""
|
||||
parser = SubcommandsOptionParser()
|
||||
parser.add_format_option(flags=('--format-item',), target=library.Item)
|
||||
parser.add_format_option(flags=('--format-album',), target=library.Album)
|
||||
parser.add_option('-l', '--library', dest='library',
|
||||
help='library database file to use')
|
||||
help=u'library database file to use')
|
||||
parser.add_option('-d', '--directory', dest='directory',
|
||||
help="destination music directory")
|
||||
parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
|
||||
help='print debugging information')
|
||||
help=u"destination music directory")
|
||||
parser.add_option('-v', '--verbose', dest='verbose', action='count',
|
||||
help=u'log more details (use twice for even more)')
|
||||
parser.add_option('-c', '--config', dest='config',
|
||||
help='path to configuration file')
|
||||
help=u'path to configuration file')
|
||||
parser.add_option('-h', '--help', dest='help', action='store_true',
|
||||
help='how this help message and exit')
|
||||
help=u'show this help message and exit')
|
||||
parser.add_option('--version', dest='version', action='store_true',
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
|
||||
@@ -924,10 +1194,12 @@ def _raw_main(args, lib=None):
|
||||
# Special case for the `config --edit` command: bypass _setup so
|
||||
# that an invalid configuration does not prevent the editor from
|
||||
# starting.
|
||||
if subargs[0] == 'config' and ('-e' in subargs or '--edit' in subargs):
|
||||
if subargs and subargs[0] == 'config' \
|
||||
and ('-e' in subargs or '--edit' in subargs):
|
||||
from beets.ui.commands import config_edit
|
||||
return config_edit()
|
||||
|
||||
test_lib = bool(lib)
|
||||
subcommands, plugins, lib = _setup(options, lib)
|
||||
parser.add_subcommand(*subcommands)
|
||||
|
||||
@@ -935,6 +1207,9 @@ def _raw_main(args, lib=None):
|
||||
subcommand.func(lib, suboptions, subargs)
|
||||
|
||||
plugins.send('cli_exit', lib=lib)
|
||||
if not test_lib:
|
||||
# Clean up the library unless it came from the test harness.
|
||||
lib._close()
|
||||
|
||||
|
||||
def main(args=None):
|
||||
@@ -945,7 +1220,7 @@ def main(args=None):
|
||||
_raw_main(args)
|
||||
except UserError as exc:
|
||||
message = exc.args[0] if exc.args else None
|
||||
log.error(u'error: {0}'.format(message))
|
||||
log.error(u'error: {0}', message)
|
||||
sys.exit(1)
|
||||
except util.HumanReadableException as exc:
|
||||
exc.log(log)
|
||||
@@ -953,11 +1228,14 @@ def main(args=None):
|
||||
except library.FileOperationError as exc:
|
||||
# These errors have reasonable human-readable descriptions, but
|
||||
# we still want to log their tracebacks for debugging.
|
||||
log.debug(traceback.format_exc())
|
||||
log.error(exc)
|
||||
log.debug('{}', traceback.format_exc())
|
||||
log.error('{}', exc)
|
||||
sys.exit(1)
|
||||
except confit.ConfigError as exc:
|
||||
log.error(u'configuration error: {0}'.format(exc))
|
||||
log.error(u'configuration error: {0}', exc)
|
||||
sys.exit(1)
|
||||
except db_query.InvalidQueryError as exc:
|
||||
log.error(u'invalid query: {0}', exc)
|
||||
sys.exit(1)
|
||||
except IOError as exc:
|
||||
if exc.errno == errno.EPIPE:
|
||||
@@ -967,4 +1245,4 @@ def main(args=None):
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
# Silently ignore ^C except in verbose mode.
|
||||
log.debug(traceback.format_exc())
|
||||
log.debug(u'{}', traceback.format_exc())
|
||||
|
||||
Reference in New Issue
Block a user