Adrian Sampson committed 0286eb6

changelog/cleanup/fixes for #209

The major functional change here is how files move around when in keep_new
mode. Now, files are first moved to the destination directory and then
copied/transcoded back into the library.

This avoids problems where naming conflicts could occur when transcoding from
MP3 to MP3 (and thus not changing the filename).

Comments (0)

Files changed (3)


 from beets.plugins import BeetsPlugin
 from beets import ui, util
 from beetsplug.embedart import _embed
-from beets import library
 from beets import config
 log = logging.getLogger('beets')
 _fs_lock = threading.Lock()
-def _dest_out(lib, dest_dir, item, keep_new):
-    """Path to the files outside the directory"""
+def _destination(lib, dest_dir, item, keep_new):
+    """Return the path under `dest_dir` where the file should be placed
+    (possibly after conversion).
+    """
+    dest = lib.destination(item, basedir=dest_dir)
     if keep_new:
-        return os.path.join(dest_dir, lib.destination(item, fragment=True))
-    dest = os.path.join(dest_dir, lib.destination(item, fragment=True))
-    return os.path.splitext(dest)[0] + '.mp3'
-def _dest_converted(lib, dest_dir, item, keep_new):
-    """Path to the newly converted files"""
-    if keep_new:
-        dest = lib.destination(item)
+        # When we're keeping the converted file, no extension munging
+        # occurs.
+        return dest
+    else:
+        # Otherwise, replace the extension with .mp3.
         return os.path.splitext(dest)[0] + '.mp3'
-    return _dest_out(lib, dest_dir, item, keep_new)
 def encode(source, dest):'Started encoding {0}'.format(util.displayable_path(source)))
 def convert_item(lib, dest_dir, keep_new):
     while True:
         item = yield
+        dest = _destination(lib, dest_dir, item, keep_new)
-        dest_converted = _dest_converted(lib, dest_dir, item, keep_new)
-        dest_out = _dest_out(lib, dest_dir, item, keep_new)
-        if os.path.exists(util.syspath(dest_out)):
+        if os.path.exists(util.syspath(dest)):
   'Skipping {0} (target file exists)'.format(
         # time. (The existence check is not atomic with the directory
         # creation inside this function.)
         with _fs_lock:
-            util.mkdirall(dest_out)
+            util.mkdirall(dest)
+        # When keeping the new file in the library, we first move the
+        # current (pristine) file to the destination. We'll then copy it
+        # back to its old path or transcode it to a new path.
+        if keep_new:
+  'Moving to {0}'.
+                     format(util.displayable_path(dest)))
+            util.move(item.path, dest)
         maxbr = config['convert']['max_bitrate'].get(int)
         if item.format == 'MP3' and item.bitrate < 1000 * maxbr:
+            # No transcoding necessary.
   'Copying {0}'.format(util.displayable_path(item.path)))
-            util.copy(item.path, dest_out)
+            if keep_new:
+                util.copy(dest, item.path)
+            else:
+                util.copy(item.path, dest)
-            encode(item.path, dest_converted)
+            if keep_new:
+                item.path = os.path.splitext(item.path)[0] + '.mp3'
+                encode(dest, item.path)
+            else:
+                encode(item.path, dest)
-            if keep_new:
-      'Moving to destination {0}'.
-                         format(util.displayable_path(dest_out)))
-                util.move(item.path, dest_out)
-        item.path = dest_converted
+        # Write tags from the database to the converted file.
+        if not keep_new:
+            item.path = dest
         if config['convert']['embed']:
                 if artpath:
                     _embed(artpath, [item])
-        if keep_new:
-  'Updating new format {0}'.format(item.format))
-            item.write()
 def convert_func(lib, opts, args):
     dest = opts.dest if opts.dest is not None else \
-        config['convert']['dest'].get()
+            config['convert']['dest'].get()
     if not dest:
         raise ui.UserError('no convert destination set')
     threads = opts.threads if opts.threads is not None else \
-        config['convert']['threads'].get(int)
+            config['convert']['threads'].get(int)
     keep_new = opts.keep_new
     ui.commands.list_items(lib, ui.decargs(args), opts.album, None)


   track in MusicBrainz and updates your library to reflect it. This can help
   you easily correct errors that have been fixed in the MB database. Thanks to
   Jakob Schnitzer.
+* :doc:`/plugins/convert`: A new ``--keep-new`` option lets you store
+  transcoded files in your library while backing up the originals (instead of
+  vice-versa). Thanks to Lucas Duailibe.
 * :doc:`/plugins/echonest_tempo`: API errors now issue a warning instead of
   exiting with an exception. We also avoid an error when track metadata
   contains newlines.


 To convert a part of your collection, run ``beet convert QUERY``. This
 will display all items matching ``QUERY`` and ask you for confirmation before
 starting the conversion. The ``-a`` (or ``--album``) option causes the command
-to match albums instead of tracks. The ``-k`` (or ``--keep-new``) allows you to
-keep the new, converted, files in your library and move the origin files to the
-destination directory.
+to match albums instead of tracks.
 The ``-t`` (``--threads``) and ``-d`` (``--dest``) options allow you to specify
 or overwrite the respective configuration options.
+By default, the command places converted files into the destination directory
+and leaves your library pristine. To instead back up your original files into
+the destination directory and keep converted files in your library, use the
+``-k`` (or ``--keep-new``) option.