mirror of
https://github.com/beetbox/beets.git
synced 2025-12-12 03:24:44 +01:00
Merge pull request #891 from Dietr1ch/master
[Improvement] --pretend option for the convert plugin
This commit is contained in:
commit
12a375f4ed
2 changed files with 67 additions and 23 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue