diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 428de312a..6b8cf6647 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -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', diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 9892cc001..8fce6d19f 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -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',