[Improvement] --pretend option for the convert plugin

Partially resolves #877 showing:
  - Directory creation
  - Copies
  - Deletes
  - Moves
  - Encodings

Information about tagging and plugins on _after_convert_ is not
currently shown. That requires changing the plugins to support the
pretend option, so a lot of work may be needed and it doesn't seem to be
helpful enough for me.
This commit is contained in:
Dietrich Daroch 2014-07-28 20:13:15 -04:00
parent 51123d901b
commit f554e2e4a0
2 changed files with 57 additions and 19 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,22 @@ 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 +401,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 +417,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 +439,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 +447,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 +640,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 +655,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

@ -83,7 +83,7 @@ def get_format():
)
def encode(source, dest):
def encode(source, dest, pretend=False):
"""Encode ``source`` to ``dest`` using the command from ``get_format()``.
Raises an ``ui.UserError`` if the command was not found and a
@ -92,7 +92,7 @@ def encode(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, _ = get_format()
@ -105,7 +105,9 @@ def encode(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...'
@ -118,7 +120,7 @@ def encode(source, dest):
u'convert: could invoke ffmpeg: {0}'.format(exc)
)
if not quiet:
if not quiet and not pretend:
log.info(u'Finished encoding {0}'.format(
util.displayable_path(source))
)
@ -134,7 +136,7 @@ def should_transcode(item):
item.bitrate >= 1000 * maxbr
def convert_item(dest_dir, keep_new, path_formats):
def convert_item(dest_dir, keep_new, path_formats, pretend=False):
while True:
item = yield
dest = _destination(dest_dir, item, keep_new, path_formats)
@ -149,7 +151,7 @@ def convert_item(dest_dir, keep_new, path_formats):
# time. (The existence check is not atomic with the directory
# creation inside this function.)
with _fs_lock:
util.mkdirall(dest)
util.mkdirall(dest, pretend)
# When keeping the new file in the library, we first move the
# current (pristine) file to the destination. We'll then copy it
@ -157,7 +159,7 @@ def convert_item(dest_dir, keep_new, path_formats):
if keep_new:
log.info(u'Moving to {0}'.
format(util.displayable_path(dest)))
util.move(item.path, dest)
util.move(item.path, dest, pretend)
original = dest
_, ext = get_format()
converted = os.path.splitext(item.path)[0] + ext
@ -168,13 +170,17 @@ def convert_item(dest_dir, keep_new, path_formats):
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(original, converted)
encode(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)
@ -229,17 +235,21 @@ def convert_func(lib, opts, args):
else:
path_formats = ui.get_path_formats(config['convert']['paths'])
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 = [convert_item(dest, keep_new, path_formats)
for i in range(threads)]
convert = [convert_item(dest, keep_new, path_formats, pretend)
for _ in range(threads)]
pipe = util.pipeline.Pipeline([items, convert])
pipe.run_parallel()
@ -249,6 +259,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': {
@ -295,6 +306,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',