mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-20 07:54:01 +01:00
Updated beets library to the latest, added musicbrainzngs to the libs
This commit is contained in:
+137
-35
@@ -26,23 +26,36 @@ from difflib import SequenceMatcher
|
||||
import logging
|
||||
import sqlite3
|
||||
import errno
|
||||
import re
|
||||
|
||||
from lib.beets import library
|
||||
from lib.beets import plugins
|
||||
from lib.beets import util
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import colorama
|
||||
colorama.init()
|
||||
|
||||
# Constants.
|
||||
CONFIG_PATH_VAR = 'BEETSCONFIG'
|
||||
DEFAULT_CONFIG_FILE = os.path.expanduser('~/.beetsconfig')
|
||||
DEFAULT_LIBRARY = '~/.beetsmusic.blb'
|
||||
DEFAULT_DIRECTORY = '~/Music'
|
||||
DEFAULT_PATH_FORMATS = {
|
||||
'default': '$albumartist/$album/$track $title',
|
||||
'comp': 'Compilations/$album/$track $title',
|
||||
'singleton': 'Non-Album/$artist/$title',
|
||||
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, '$albumartist/$album/$track $title'),
|
||||
(PF_KEY_QUERIES['singleton'], 'Non-Album/$artist/$title'),
|
||||
(PF_KEY_QUERIES['comp'], 'Compilations/$album/$track $title'),
|
||||
]
|
||||
DEFAULT_ART_FILENAME = 'cover'
|
||||
|
||||
DEFAULT_TIMEOUT = 5.0
|
||||
NULL_REPLACE = '<strip>'
|
||||
|
||||
# UI exception. Commands should throw this in order to display
|
||||
# nonrecoverable errors to the user.
|
||||
@@ -177,7 +190,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
||||
prompt_part_lengths.append(len(tmpl % str(default)))
|
||||
else:
|
||||
prompt_parts.append('# selection')
|
||||
prompt_part_lengths.append(prompt_parts[-1])
|
||||
prompt_part_lengths.append(len(prompt_parts[-1]))
|
||||
prompt_parts += capitalized
|
||||
prompt_part_lengths += [len(s) for s in options]
|
||||
|
||||
@@ -257,10 +270,10 @@ def input_yn(prompt, require=False, color=False):
|
||||
return sel == 'y'
|
||||
|
||||
def config_val(config, section, name, default, vtype=None):
|
||||
"""Queries the configuration file for a value (given by the
|
||||
section and name). If no value is present, returns default.
|
||||
vtype optionally specifies the return type (although only bool
|
||||
is supported for now).
|
||||
"""Queries the configuration file for a value (given by the section
|
||||
and name). If no value is present, returns default. vtype
|
||||
optionally specifies the return type (although only ``bool`` and
|
||||
``list`` are supported for now).
|
||||
"""
|
||||
if not config.has_section(section):
|
||||
config.add_section(section)
|
||||
@@ -268,8 +281,12 @@ def config_val(config, section, name, default, vtype=None):
|
||||
try:
|
||||
if vtype is bool:
|
||||
return config.getboolean(section, name)
|
||||
elif vtype is list:
|
||||
# Whitespace-separated strings.
|
||||
strval = config.get(section, name, True)
|
||||
return strval.split()
|
||||
else:
|
||||
return config.get(section, name)
|
||||
return config.get(section, name, True)
|
||||
except ConfigParser.NoOptionError:
|
||||
return default
|
||||
|
||||
@@ -284,7 +301,7 @@ def human_bytes(size):
|
||||
|
||||
def human_seconds(interval):
|
||||
"""Formats interval, a number of seconds, as a human-readable time
|
||||
interval.
|
||||
interval using English words.
|
||||
"""
|
||||
units = [
|
||||
(1, 'second'),
|
||||
@@ -308,6 +325,13 @@ def human_seconds(interval):
|
||||
|
||||
return "%3.1f %ss" % (interval, suffix)
|
||||
|
||||
def human_seconds_short(interval):
|
||||
"""Formats a number of seconds as a short human-readable M:SS
|
||||
string.
|
||||
"""
|
||||
interval = int(interval)
|
||||
return u'%i:%02i' % (interval // 60, interval % 60)
|
||||
|
||||
# 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.)
|
||||
@@ -369,6 +393,84 @@ def colordiff(a, b, highlight='red'):
|
||||
|
||||
return u''.join(a_out), u''.join(b_out)
|
||||
|
||||
def default_paths(pathmod=None):
|
||||
"""Produces the appropriate default config, library, and directory
|
||||
paths for the current system. On Unix, this is always in ~. On
|
||||
Windows, tries ~ first and then $APPDATA for the config and library
|
||||
files (for backwards compatibility).
|
||||
"""
|
||||
pathmod = pathmod or os.path
|
||||
windows = pathmod.__name__ == 'ntpath'
|
||||
if windows:
|
||||
windata = os.environ.get('APPDATA') or '~'
|
||||
|
||||
# Shorthand for joining paths.
|
||||
def exp(*vals):
|
||||
return pathmod.expanduser(pathmod.join(*vals))
|
||||
|
||||
config = exp('~', DEFAULT_CONFIG_FILENAME_UNIX)
|
||||
if windows and not pathmod.exists(config):
|
||||
config = exp(windata, DEFAULT_CONFIG_FILENAME_WINDOWS)
|
||||
|
||||
libpath = exp('~', DEFAULT_LIBRARY_FILENAME_UNIX)
|
||||
if windows and not pathmod.exists(libpath):
|
||||
libpath = exp(windata, DEFAULT_LIBRARY_FILENAME_WINDOWS)
|
||||
|
||||
libdir = exp('~', DEFAULT_DIRECTORY_NAME)
|
||||
|
||||
return config, libpath, libdir
|
||||
|
||||
def _get_replacements(config):
|
||||
"""Given a ConfigParser, get the list of replacement pairs. If no
|
||||
replacements are specified, returns None. Otherwise, returns a list
|
||||
of (compiled regex, replacement string) pairs.
|
||||
"""
|
||||
repl_string = config_val(config, 'beets', 'replace', None)
|
||||
if not repl_string:
|
||||
return
|
||||
|
||||
parts = repl_string.strip().split()
|
||||
if not parts:
|
||||
return
|
||||
if len(parts) % 2 != 0:
|
||||
# Must have an even number of parts.
|
||||
raise UserError(u'"replace" config value must consist of'
|
||||
u' pattern/replacement pairs')
|
||||
|
||||
out = []
|
||||
for index in xrange(0, len(parts), 2):
|
||||
pattern = parts[index]
|
||||
replacement = parts[index+1]
|
||||
if replacement.lower() == NULL_REPLACE:
|
||||
replacement = ''
|
||||
out.append((re.compile(pattern), replacement))
|
||||
return out
|
||||
|
||||
def _get_path_formats(config):
|
||||
"""Returns a list of path formats (query/template pairs); reflecting
|
||||
the config's specified path formats.
|
||||
"""
|
||||
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)]
|
||||
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):
|
||||
if key in PF_KEY_QUERIES:
|
||||
# Special values that indicate simple queries.
|
||||
key = PF_KEY_QUERIES[key]
|
||||
elif key != library.PF_KEY_DEFAULT:
|
||||
# For non-special keys (literal queries), the _
|
||||
# character denotes a :.
|
||||
key = key.replace('_', ':')
|
||||
custom_path_formats.append((key, value))
|
||||
path_formats = custom_path_formats + path_formats
|
||||
return path_formats
|
||||
|
||||
|
||||
# Subcommand parsing infrastructure.
|
||||
|
||||
@@ -536,7 +638,10 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
||||
def main(args=None, configfh=None):
|
||||
"""Run the main command-line interface for beets."""
|
||||
# Get the default subcommands.
|
||||
from beets.ui.commands import default_commands
|
||||
from lib.beets.ui.commands import default_commands
|
||||
|
||||
# Get default file paths.
|
||||
default_config, default_libpath, default_dir = default_paths()
|
||||
|
||||
# Read defaults from config file.
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
@@ -545,7 +650,7 @@ def main(args=None, configfh=None):
|
||||
elif CONFIG_PATH_VAR in os.environ:
|
||||
configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR])
|
||||
else:
|
||||
configpath = DEFAULT_CONFIG_FILE
|
||||
configpath = default_config
|
||||
if configpath:
|
||||
configpath = util.syspath(configpath)
|
||||
if os.path.exists(util.syspath(configpath)):
|
||||
@@ -574,8 +679,6 @@ def main(args=None, configfh=None):
|
||||
help='library database file to use')
|
||||
parser.add_option('-d', '--directory', dest='directory',
|
||||
help="destination music directory")
|
||||
parser.add_option('-p', '--pathformat', dest='path_format',
|
||||
help="destination path format string")
|
||||
parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
|
||||
help='print debugging information')
|
||||
|
||||
@@ -584,30 +687,26 @@ def main(args=None, configfh=None):
|
||||
|
||||
# Open library file.
|
||||
libpath = options.libpath or \
|
||||
config_val(config, 'beets', 'library', DEFAULT_LIBRARY)
|
||||
config_val(config, 'beets', 'library', default_libpath)
|
||||
directory = options.directory or \
|
||||
config_val(config, 'beets', 'directory', DEFAULT_DIRECTORY)
|
||||
legacy_path_format = config_val(config, 'beets', 'path_format', None)
|
||||
if options.path_format:
|
||||
# If given, -p overrides all path format settings
|
||||
path_formats = {'default': options.path_format}
|
||||
else:
|
||||
if legacy_path_format:
|
||||
# Old path formats override the default values.
|
||||
path_formats = {'default': legacy_path_format}
|
||||
else:
|
||||
# If no legacy path format, use the defaults instead.
|
||||
path_formats = DEFAULT_PATH_FORMATS
|
||||
if config.has_section('paths'):
|
||||
path_formats.update(config.items('paths'))
|
||||
config_val(config, 'beets', 'directory', default_dir)
|
||||
path_formats = _get_path_formats(config)
|
||||
art_filename = \
|
||||
config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME)
|
||||
lib_timeout = config_val(config, 'beets', 'timeout', DEFAULT_TIMEOUT)
|
||||
replacements = _get_replacements(config)
|
||||
try:
|
||||
lib_timeout = float(lib_timeout)
|
||||
except ValueError:
|
||||
lib_timeout = DEFAULT_TIMEOUT
|
||||
db_path = os.path.expanduser(libpath)
|
||||
try:
|
||||
lib = library.Library(db_path,
|
||||
directory,
|
||||
path_formats,
|
||||
art_filename)
|
||||
art_filename,
|
||||
lib_timeout,
|
||||
replacements)
|
||||
except sqlite3.OperationalError:
|
||||
raise UserError("database file %s could not be opened" % db_path)
|
||||
|
||||
@@ -617,6 +716,9 @@ def main(args=None, configfh=None):
|
||||
log.setLevel(logging.DEBUG)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
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:
|
||||
|
||||
Executable → Regular
+184
-62
@@ -29,7 +29,8 @@ 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
|
||||
from lib.beets.util import syspath, normpath, ancestry, displayable_path
|
||||
from lib.beets.util.functemplate import Template
|
||||
from lib.beets import library
|
||||
|
||||
# Global logger.
|
||||
@@ -97,9 +98,14 @@ DEFAULT_IMPORT_RESUME = None # "ask"
|
||||
DEFAULT_IMPORT_INCREMENTAL = False
|
||||
DEFAULT_THREADED = True
|
||||
DEFAULT_COLOR = True
|
||||
DEFAULT_IGNORE = [
|
||||
'.*', '*~',
|
||||
]
|
||||
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
|
||||
PARTIAL_MATCH_MESSAGE = u'(partial match!)'
|
||||
|
||||
# Importer utilities and support.
|
||||
|
||||
def dist_string(dist, color):
|
||||
@@ -121,13 +127,29 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
tags are changed from (cur_artist, cur_album, items) to info with
|
||||
distance dist.
|
||||
"""
|
||||
def show_album(artist, album):
|
||||
def show_album(artist, album, partial=False):
|
||||
if artist:
|
||||
print_(' %s - %s' % (artist, album))
|
||||
album_description = u' %s - %s' % (artist, album)
|
||||
elif album:
|
||||
print_(' %s' % album)
|
||||
album_description = u' %s' % album
|
||||
else:
|
||||
print_(' (unknown album)')
|
||||
album_description = u' (unknown album)'
|
||||
|
||||
# Add a suffix if this is a partial match.
|
||||
if partial:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
else:
|
||||
warning = None
|
||||
if color and warning:
|
||||
warning = ui.colorize('yellow', warning)
|
||||
|
||||
out = album_description
|
||||
if warning:
|
||||
out += u' ' + warning
|
||||
print_(out)
|
||||
|
||||
# Record if the match is partial or not.
|
||||
partial_match = None in items
|
||||
|
||||
# Identify the album in question.
|
||||
if cur_artist != info.artist or \
|
||||
@@ -147,17 +169,35 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
print_("To:")
|
||||
show_album(artist_r, album_r)
|
||||
else:
|
||||
print_("Tagging: %s - %s" % (info.artist, info.album))
|
||||
message = u"Tagging: %s - %s" % (info.artist, info.album)
|
||||
if partial_match:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
warning = ui.colorize('yellow', PARTIAL_MATCH_MESSAGE)
|
||||
message += u' ' + warning
|
||||
print_(message)
|
||||
|
||||
# Distance/similarity.
|
||||
print_('(Similarity: %s)' % dist_string(dist, color))
|
||||
|
||||
# Tracks.
|
||||
missing_tracks = []
|
||||
for i, (item, track_info) in enumerate(zip(items, info.tracks)):
|
||||
cur_track = str(item.track)
|
||||
new_track = str(i+1)
|
||||
if not item:
|
||||
missing_tracks.append((i, track_info))
|
||||
continue
|
||||
|
||||
# Get displayable LHS and RHS values.
|
||||
cur_track = unicode(item.track)
|
||||
new_track = unicode(i+1)
|
||||
cur_title = item.title
|
||||
new_title = track_info.title
|
||||
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:
|
||||
cur_length = ui.colorize('red', cur_length)
|
||||
new_length = ui.colorize('red', new_length)
|
||||
|
||||
# Possibly colorize changes.
|
||||
if color:
|
||||
@@ -168,16 +208,31 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
||||
|
||||
# Show filename (non-colorized) when title is not set.
|
||||
if not item.title.strip():
|
||||
cur_title = os.path.basename(item.path)
|
||||
cur_title = displayable_path(os.path.basename(item.path))
|
||||
|
||||
if cur_title != new_title and cur_track != new_track:
|
||||
print_(" * %s (%s) -> %s (%s)" % (
|
||||
cur_title, cur_track, new_title, new_track
|
||||
))
|
||||
elif cur_title != new_title:
|
||||
print_(" * %s -> %s" % (cur_title, new_title))
|
||||
elif cur_track != new_track:
|
||||
print_(" * %s (%s -> %s)" % (item.title, cur_track, new_track))
|
||||
if cur_title != new_title:
|
||||
lhs, rhs = cur_title, new_title
|
||||
if cur_track != new_track:
|
||||
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:
|
||||
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)
|
||||
if display:
|
||||
print_(line)
|
||||
for i, track_info in missing_tracks:
|
||||
line = u' * Missing track: %s (%d)' % (track_info.title, i+1)
|
||||
if color:
|
||||
line = ui.colorize('yellow', line)
|
||||
print_(line)
|
||||
|
||||
def show_item_change(item, info, dist, color):
|
||||
"""Print out the change that would occur by tagging `item` with the
|
||||
@@ -281,22 +336,21 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
(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)))
|
||||
print_('%i. %s - %s (%s)' % (i+1, info.artist,
|
||||
info.title, dist_string(dist, 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'])
|
||||
line = '%i. %s - %s' % (i+1, info.artist, info.album)
|
||||
|
||||
# Label and year disambiguation, if available.
|
||||
label, year = None, None
|
||||
if 'label' in info:
|
||||
label = info['label']
|
||||
if 'year' in info and info['year']:
|
||||
year = unicode(info['year'])
|
||||
if info.label:
|
||||
label = info.label
|
||||
if info.year:
|
||||
year = unicode(info.year)
|
||||
if label and year:
|
||||
line += u' [%s, %s]' % (label, year)
|
||||
elif label:
|
||||
@@ -305,6 +359,14 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
||||
line += u' [%s]' % year
|
||||
|
||||
line += ' (%s)' % dist_string(dist, color)
|
||||
|
||||
# Point out the partial matches.
|
||||
if None in items:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
warning = ui.colorize('yellow', warning)
|
||||
line += u' %s' % warning
|
||||
|
||||
print_(line)
|
||||
|
||||
# Ask the user for a choice.
|
||||
@@ -502,11 +564,40 @@ def choose_item(task, config):
|
||||
assert not isinstance(choice, importer.action)
|
||||
return choice
|
||||
|
||||
def resolve_duplicate(task, config):
|
||||
"""Decide what to do when a new album or item seems similar to one
|
||||
that's already in the library.
|
||||
"""
|
||||
log.warn("This %s is already in the library!" %
|
||||
("album" if task.is_album else "item"))
|
||||
|
||||
if config.quiet:
|
||||
# In quiet mode, don't prompt -- just skip.
|
||||
log.info('Skipping.')
|
||||
sel = 's'
|
||||
else:
|
||||
sel = ui.input_options(
|
||||
('Skip new', 'Keep both', 'Remove old'),
|
||||
color=config.color
|
||||
)
|
||||
|
||||
if sel == 's':
|
||||
# Skip new.
|
||||
task.set_choice(importer.action.SKIP)
|
||||
elif sel == 'k':
|
||||
# Keep both. Do nothing; leave the choice intact.
|
||||
pass
|
||||
elif sel == 'r':
|
||||
# Remove old.
|
||||
task.remove_duplicates = True
|
||||
else:
|
||||
assert False
|
||||
|
||||
# The import command.
|
||||
|
||||
def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
color, delete, quiet, resume, quiet_fallback, singletons,
|
||||
timid, query, incremental):
|
||||
timid, query, incremental, ignore):
|
||||
"""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
|
||||
@@ -537,7 +628,11 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
# Open the log.
|
||||
if logpath:
|
||||
logpath = normpath(logpath)
|
||||
logfile = open(syspath(logpath), 'a')
|
||||
try:
|
||||
logfile = open(syspath(logpath), 'a')
|
||||
except IOError:
|
||||
raise ui.UserError(u"could not open log file for writing: %s" %
|
||||
displayable_path(logpath))
|
||||
print >>logfile, 'import started', time.asctime()
|
||||
else:
|
||||
logfile = None
|
||||
@@ -546,34 +641,38 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
if resume is None and quiet:
|
||||
resume = False
|
||||
|
||||
# Perform the import.
|
||||
importer.run_import(
|
||||
lib = lib,
|
||||
paths = paths,
|
||||
resume = resume,
|
||||
logfile = logfile,
|
||||
color = color,
|
||||
quiet = quiet,
|
||||
quiet_fallback = quiet_fallback,
|
||||
copy = copy,
|
||||
write = write,
|
||||
art = art,
|
||||
delete = delete,
|
||||
threaded = threaded,
|
||||
autot = autot,
|
||||
choose_match_func = choose_match,
|
||||
should_resume_func = should_resume,
|
||||
singletons = singletons,
|
||||
timid = timid,
|
||||
choose_item_func = choose_item,
|
||||
query = query,
|
||||
incremental = incremental,
|
||||
)
|
||||
try:
|
||||
# Perform the import.
|
||||
importer.run_import(
|
||||
lib = lib,
|
||||
paths = paths,
|
||||
resume = resume,
|
||||
logfile = logfile,
|
||||
color = color,
|
||||
quiet = quiet,
|
||||
quiet_fallback = quiet_fallback,
|
||||
copy = copy,
|
||||
write = write,
|
||||
art = art,
|
||||
delete = delete,
|
||||
threaded = threaded,
|
||||
autot = autot,
|
||||
choose_match_func = choose_match,
|
||||
should_resume_func = should_resume,
|
||||
singletons = singletons,
|
||||
timid = timid,
|
||||
choose_item_func = choose_item,
|
||||
query = query,
|
||||
incremental = incremental,
|
||||
ignore = ignore,
|
||||
resolve_duplicate_func = resolve_duplicate,
|
||||
)
|
||||
|
||||
# If we were logging, close the file.
|
||||
if logfile:
|
||||
print >>logfile, ''
|
||||
logfile.close()
|
||||
finally:
|
||||
# If we were logging, close the file.
|
||||
if logfile:
|
||||
print >>logfile, ''
|
||||
logfile.close()
|
||||
|
||||
# Emit event.
|
||||
plugins.send('import', lib=lib, paths=paths)
|
||||
@@ -641,6 +740,7 @@ def import_func(lib, config, opts, args):
|
||||
incremental = opts.incremental if opts.incremental is not None else \
|
||||
ui.config_val(config, 'beets', 'import_incremental',
|
||||
DEFAULT_IMPORT_INCREMENTAL, bool)
|
||||
ignore = ui.config_val(config, 'beets', 'ignore', DEFAULT_IGNORE, list)
|
||||
|
||||
# Resume has three options: yes, no, and "ask" (None).
|
||||
resume = opts.resume if opts.resume is not None else \
|
||||
@@ -667,38 +767,48 @@ def import_func(lib, config, opts, args):
|
||||
|
||||
import_files(lib, paths, copy, write, autot, logpath, art, threaded,
|
||||
color, delete, quiet, resume, quiet_fallback, singletons,
|
||||
timid, query, incremental)
|
||||
timid, query, incremental, ignore)
|
||||
import_cmd.func = import_func
|
||||
default_commands.append(import_cmd)
|
||||
|
||||
|
||||
# list: Query and show library contents.
|
||||
|
||||
def list_items(lib, query, album, path):
|
||||
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:
|
||||
for album in lib.albums(query):
|
||||
if path:
|
||||
print_(album.item_dir())
|
||||
else:
|
||||
print_(album.albumartist + u' - ' + album.album)
|
||||
elif fmt is not None:
|
||||
print_(template.substitute(album._record))
|
||||
else:
|
||||
for item in lib.items(query):
|
||||
if path:
|
||||
print_(item.path)
|
||||
else:
|
||||
print_(item.artist + u' - ' + item.album + u' - ' + item.title)
|
||||
elif fmt is not None:
|
||||
print_(template.substitute(item.record))
|
||||
|
||||
list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',))
|
||||
list_cmd.parser.add_option('-a', '--album', action='store_true',
|
||||
help='show matching albums instead of tracks')
|
||||
list_cmd.parser.add_option('-p', '--path', action='store_true',
|
||||
help='print paths for matched items or albums')
|
||||
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)
|
||||
list_items(lib, decargs(args), opts.album, opts.path, opts.format)
|
||||
list_cmd.func = list_func
|
||||
default_commands.append(list_cmd)
|
||||
|
||||
@@ -722,6 +832,12 @@ def update_items(lib, query, album, move, color, pretend):
|
||||
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()
|
||||
@@ -755,6 +871,12 @@ def update_items(lib, query, album, move, color, pretend):
|
||||
|
||||
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:
|
||||
@@ -885,7 +1007,7 @@ default_commands.append(stats_cmd)
|
||||
# version: Show current beets version.
|
||||
|
||||
def show_version(lib, config, opts, args):
|
||||
print 'beets version %s' % beets.__version__
|
||||
print 'beets version %s' % lib.beets.__version__
|
||||
# Show plugins.
|
||||
names = []
|
||||
for plugin in plugins.find_plugins():
|
||||
|
||||
Reference in New Issue
Block a user