Merge pull request #891 from Dietr1ch/master

[Improvement] --pretend option for the convert plugin
This commit is contained in:
Adrian Sampson 2014-08-10 16:28:07 -07:00
commit 12a375f4ed
2 changed files with 67 additions and 23 deletions

View file

@ -24,6 +24,7 @@ from collections import defaultdict
import traceback
import subprocess
import platform
from sets import Set
MAX_FILENAME_LENGTH = 200
@ -201,10 +202,25 @@ def sorted_walk(path, ignore=(), logger=None):
yield res
def mkdirall(path):
# We don't create directories on dry-runs, but we must pretend they exist
directories_created = Set()
def mkdirall(path, pretend=False):
"""Make all the enclosing directories of path (like mkdir -p on the
parent).
"""
if pretend:
# directory = syspath(ancestry(path)[-1]) # "dirname"
# This seems cleaner but MAY have differences on symlinks (leading to
# an equivalent result)
directory = os.path.dirname(path)
if directory not in directories_created:
directories_created.add(directory)
# This is not a "raw" translation, but it's brief one
print("mkdir -p '%s'" % (directory))
return
for ancestor in ancestry(path):
if not os.path.isdir(syspath(ancestor)):
try:
@ -388,10 +404,13 @@ def samefile(p1, p2):
return shutil._samefile(syspath(p1), syspath(p2))
def remove(path, soft=True):
def remove(path, soft=True, pretend=False):
"""Remove the file. If `soft`, then no error will be raised if the
file does not exist.
"""
if pretend:
print("rm '%s'" % (path))
return
path = syspath(path)
if soft and not os.path.exists(path):
return
@ -401,12 +420,15 @@ def remove(path, soft=True):
raise FilesystemError(exc, 'delete', (path,), traceback.format_exc())
def copy(path, dest, replace=False):
def copy(path, dest, replace=False, pretend=False):
"""Copy a plain file. Permissions are not copied. If `dest` already
exists, raises a FilesystemError unless `replace` is True. Has no
effect if `path` is the same as `dest`. Paths are translated to
system paths before the syscall.
"""
if pretend:
print("cp '%s' '%s'" % (path, dest))
return
if samefile(path, dest):
return
path = syspath(path)
@ -420,7 +442,7 @@ def copy(path, dest, replace=False):
traceback.format_exc())
def move(path, dest, replace=False):
def move(path, dest, replace=False, pretend=False):
"""Rename a file. `dest` may not be a directory. If `dest` already
exists, raises an OSError unless `replace` is True. Has no effect if
`path` is the same as `dest`. If the paths are on different
@ -428,6 +450,9 @@ def move(path, dest, replace=False):
instead, in which case metadata will *not* be preserved. Paths are
translated to system paths.
"""
if pretend:
print("mv '%s' '%s'" % (path, dest))
return
if samefile(path, dest):
return
path = syspath(path)
@ -618,7 +643,7 @@ def cpu_count():
return 1
def command_output(cmd, shell=False):
def command_output(cmd, shell=False, pretend=False):
"""Runs the command and returns its output after it has exited.
``cmd`` is a list of arguments starting with the command names. If
@ -633,6 +658,9 @@ def command_output(cmd, shell=False):
Python 2.6 and which can have problems if lots of output is sent to
stderr.
"""
if pretend:
print(cmd)
return
with open(os.devnull, 'wb') as devnull:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=devnull,
close_fds=platform.system() != 'Windows',

View file

@ -81,7 +81,7 @@ def get_format(format=None):
return (command.encode('utf8'), extension.encode('utf8'))
def encode(command, source, dest):
def encode(command, source, dest, pretend=False):
"""Encode `source` to `dest` using command template `command`.
Raises `subprocess.CalledProcessError` if the command exited with a
@ -89,7 +89,7 @@ def encode(command, source, dest):
"""
quiet = config['convert']['quiet'].get()
if not quiet:
if not quiet and not pretend:
log.info(u'Started encoding {0}'.format(util.displayable_path(source)))
command = Template(command).safe_substitute({
@ -101,7 +101,9 @@ def encode(command, source, dest):
.format(util.displayable_path(command)))
try:
util.command_output(command, shell=True)
util.command_output(command, shell=True, pretend=pretend)
if pretend:
return
except subprocess.CalledProcessError:
# Something went wrong (probably Ctrl+C), remove temporary files
log.info(u'Encoding {0} failed. Cleaning up...'
@ -114,7 +116,7 @@ def encode(command, source, dest):
u"convert: could invoke '{0}': {0}".format(command, exc)
)
if not quiet:
if not quiet and not pretend:
log.info(u'Finished encoding {0}'.format(
util.displayable_path(source))
)
@ -130,7 +132,8 @@ def should_transcode(item):
item.bitrate >= 1000 * maxbr
def convert_item(dest_dir, keep_new, path_formats, command, ext):
def convert_item(dest_dir, keep_new, path_formats, command, ext,
pretend=False):
while True:
item = yield
dest = item.destination(basedir=dest_dir, path_formats=path_formats)
@ -150,7 +153,7 @@ def convert_item(dest_dir, keep_new, path_formats, command, ext):
# time. (The existence check is not atomic with the directory
# creation inside this function.)
with _fs_lock:
util.mkdirall(dest)
util.mkdirall(dest, pretend)
if os.path.exists(util.syspath(dest)):
log.info(u'Skipping {0} (target file exists)'.format(
@ -161,18 +164,22 @@ def convert_item(dest_dir, keep_new, path_formats, command, ext):
if keep_new:
log.info(u'Moving to {0}'.
format(util.displayable_path(original)))
util.move(item.path, original)
util.move(item.path, original, pretend)
if not should_transcode(item):
# No transcoding necessary.
log.info(u'Copying {0}'.format(util.displayable_path(item.path)))
util.copy(original, converted)
util.copy(original, converted, pretend)
else:
try:
encode(command, original, converted)
encode(command, original, converted, pretend)
except subprocess.CalledProcessError:
continue
if pretend:
# Should we add support for tagging and after_convert plugins?
continue # A yield is used at the start of the loop
# Write tags from the database to the converted file.
item.write(path=converted)
@ -230,21 +237,27 @@ def convert_func(lib, opts, args):
command, ext = get_format(opts.format)
ui.commands.list_items(lib, ui.decargs(args), opts.album, None)
pretend = opts.pretend if opts.pretend is not None else \
config['convert']['pretend'].get()
if not ui.input_yn("Convert? (Y/n)"):
return
if not pretend:
ui.commands.list_items(lib, ui.decargs(args), opts.album, None)
if not ui.input_yn("Convert? (Y/n)"):
return
if opts.album:
items = (i for a in lib.albums(ui.decargs(args)) for i in a.items())
else:
items = iter(lib.items(ui.decargs(args)))
convert_stages = []
for i in range(opts.threads):
convert_stages.append(
convert_item(opts.dest, opts.keep_new, path_formats, command, ext)
)
pipe = util.pipeline.Pipeline([items, convert_stages])
convert = [convert_item(opts.dest,
opts.keep_new,
path_formats,
command,
ext,
pretend)
for _ in range(opts.threads)]
pipe = util.pipeline.Pipeline([items, convert])
pipe.run_parallel()
@ -253,6 +266,7 @@ class ConvertPlugin(BeetsPlugin):
super(ConvertPlugin, self).__init__()
self.config.add({
u'dest': None,
u'pretend': False,
u'threads': util.cpu_count(),
u'format': u'mp3',
u'formats': {
@ -284,6 +298,8 @@ class ConvertPlugin(BeetsPlugin):
def commands(self):
cmd = ui.Subcommand('convert', help='convert to external location')
cmd.parser.add_option('-p', '--pretend', action='store_true',
help='only show what would happen')
cmd.parser.add_option('-a', '--album', action='store_true',
help='choose albums instead of tracks')
cmd.parser.add_option('-t', '--threads', action='store', type='int',